Skip to content
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

Sagepay support #42

Closed
wants to merge 9 commits into from
Closed

Conversation

mattwire
Copy link
Contributor

This is now in use on a production site and working. However, there are a number of changes to core extension files as well as the vendor library so probably can't be merged as is!

@eileenmcnaughton If you get a chance to review any of my changes that would be appreciated!

@eileenmcnaughton
Copy link
Owner

thanks @mattwire sorry I didn't get around to replying to your email!

@@ -352,6 +352,12 @@ public function sendData($data)
case 'DeliveryCountry':
$data[$key] = $data['BillingCountry'];
break;
case 'NotificationURL':
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattwire I just added this to the extension - 763d01f - which makes this change redundant.

(also fyi @nganivet

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton Ok, I agree it looks like case 'NotificationURL' is redundant but I need to pass in qfKey as an URL parameter to generate the redirect URLs correctly. Currently I'm doing that directly in CRM_Core_PaymentExtended::getNotifyURL.

@@ -639,7 +639,7 @@ public function handlePaymentNotification() {
if (empty(CRM_Core_Session::singleton()->getLoggedInContactID())) {
// Don't be too efficient on processing the ipn return.
// So far the best way of telling the difference is the session.
sleep(45);
// sleep(45);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I have 2 thoughts here - fundamental issue is ipn & browser returning at the same time, starting processing the payment at the same time & sending 2 emails.

  1. We could put ipn_delay_time in the metadata . Some processors are more concurrent than others making it more or less of an issue.
  2. we could use a mysql lock - that mostly works prior to mysql 5.7 & can be totally solid in 5.7 with perhaps a small patch - CRM_Core_Lock I believe

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand the process... But from the sagepay side they expect a response within 1 second otherwise their IPN request fails. (I spent hours trying to work out why it was timing out on their side, until I found that in their docs somewhere).

Sagepay is a 4-step process as we:

  1. Redirect to sagepay servers to make payment.
  2. Sagepay notifies CiviCRM of payment status and requests a redirect URL (via IPN).
  3. Sagepay receives IPN response (redirectURL) from CiviCRM within 1 second of request (otherwise it retries and times out).
  4. Sagepay redirects to redirectURL.

I think most redirect processors are only 3-steps (ie don't have separate steps 2-3) so maybe this is somewhat unique to sagepay? I don't generally like the idea of arbitrary delays in code (ie. sleep) so if it makes sense to use a mysql lock that might be better? Though I don't really understand the code and reason for the delay :-)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattwire I don't suppose you are using mysql 5.7.5+? I feel like using mysql locking is an appropriate way to prevent the 2 processes both processing the ipn at the same time - however, if only works properly with mysql 5.7.5+

Some discussion & links here civicrm/civicrm-core#11720 (but I have added a version of this to extension)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I've rebased sagepay branch against master, looking much cleaner now. And I've raised PR #47 to set ipn delay for sagepay

@mattwire mattwire force-pushed the sagepay branch 2 times, most recently from 4b646da to a7ae3fd Compare February 7, 2018 15:23
@@ -657,7 +657,12 @@ public function processPaymentNotification($params) {
$originalRequest = $_REQUEST;
$_REQUEST = $params;
try {
$response = $this->gateway->completePurchase($params)->send();
if ($this->gateway->supportsAcceptNotification()) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @mattwire I've just added this commit to the main repo - I'm afraid I'm cherry picking my way through your commits as time permits rather than assessing as a whole, as there are a lot of issues in there to consider

@eileenmcnaughton
Copy link
Owner

Hi @mattwire I've cherry-picked out the things that I'm comfortable with - I'll need to work with you on the remaining items before I feel sure - they probably need splitting out into issues

@eileenmcnaughton
Copy link
Owner

@mattwire do you want to rebase this - quite a bit is merged

@mattwire mattwire force-pushed the sagepay branch 2 times, most recently from aff9ef1 to f8fe1e1 Compare March 1, 2018 17:14
@eileenmcnaughton
Copy link
Owner

Ok so for the Sagepay fixes the way to do this is to submit a change to the composer.json to point towards your git repository to retrieve the Sagepay ones - if you do that I'll run composer afterwards & push. I have some reservations - mostly with the buildSignature change but I'm prepared to leave that with you to try to negotiate upstream

@mattwire mattwire force-pushed the sagepay branch 12 times, most recently from 644af07 to 630e9c0 Compare March 3, 2018 19:07
@mattwire
Copy link
Contributor Author

mattwire commented Mar 3, 2018

@eileenmcnaughton I've added a forked version of the omnipay-sagepay library via this commit: 688f917
Could you merge this and do the composer thing :-)

@@ -201,7 +201,7 @@ protected function getGoBackUrl($qfKey) {
protected function getNotifyUrl($allowLocalHost = FALSE) {
$url = CRM_Utils_System::url(
'civicrm/payment/ipn/' . $this->formatted_transaction_id . '/' . $this->_paymentProcessor['id'],
NULL,
array('qfKey' => CRM_Utils_Request::retrieve('qfKey', 'String')),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton So the session key is required for sagepay because the IPN handler returns the redirect URL to sagepay when requested, rather than using the URL via the initial transfer offsite. Any thoughts on how this could be handled?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattwire I'm trying to remove those functions that have been migrated to core from this extension - that goBackUrl is one of them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton We're talking about getNotifyUrl and not getGoBackUrl. However, I see the same applies. Basically the session key (qfKey) is essential for sagepay because (uniquely) Sagepay does the final user redirect back from their site ONLY after the IPN has completed. It needs the session key to be returned so that it can redirect the user to the CiviCRM payment confirmation page. Any idea how we could gracefully override this in core?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattwire normally the QFKey is stored in the user session before leaving the site & retrieved from there on return?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so discussion on mattermost - we should see if we can modify "next url" so that it is set to civicrm/payment/ipn/5 or equivalent and then we would not need the session key.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep - although looking at the above - there is an Omnipay-specific thing already in that function

  • 'civicrm/payment/ipn/' . $this->formatted_transaction_id . '/'

Hmm - we shouldn't be sending next url to Sagepay but returnUrl - that will figure out nextUrl

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton I don't think there is any way around putting the qfKey in getNotifyUrl:

Done a bit of debugging as I couldn't remember exactly what was going on.

No session key supplied via getNotifyUrl (standard):

  1. CiviCRM payment submitted.
  2. Goes offsite to Sagepay.
  3. Sagepay returns via ipn (eg. civicrm/payment/ipn/146/5).
  4. CiviCRM server responds to IPN via processPaymentNotification and then redirectOrExit. Both of these functions are OUTSIDE of the session so getStoredUrl in redirectOrExit fails.
  5. Sagepay receives redirect URL via $response->confirm($redirectUrl, $userMsg) and redirects the users browser to that URL, but the URL is invalid because we didn't know the session key.

Session key supplied via getNotifyUrl (modified):

  1. CiviCRM payment submitted.
  2. Goes offsite to Sagepay.
  3. Sagepay returns via ipn (eg. civicrm/payment/ipn/146/5?qfKey=abc1234).
  4. CiviCRM server responds to IPN via processPaymentNotification and then redirectOrExit. Both of these functions are OUTSIDE of the session so getStoredUrl in redirectOrExit fails. But we then try again, retrieving the session key from the request URL and now we have access to the session so it works.
  5. Sagepay receives redirect URL via $response->confirm($redirectUrl, $userMsg) and redirects the users browser to that URL which is the correct URL because it came from the stored session.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattwire so

      $output = $response->confirm($redirectUrl, $userMsg);

only got added for SagePay and it seems like it would work if getReturnUrl were returned instead.

if (empty($redirectUrl)) {
$redirectUrl = $this->getReturnSuccessUrl(CRM_Utils_Request::retrieve('qfKey', 'String'));
}
if (method_exists($response, 'confirm')) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton For sagepay we don't seem to have a stored URL (because we are in the IPN handler?) so we need to generate one here with the session key that is passed back to us via the IPN request URL.

@@ -839,7 +839,7 @@ protected function redirectOrExit($outcome, $response) {
if (empty($redirectUrl)) {
$redirectUrl = $this->getReturnSuccessUrl(CRM_Utils_Request::retrieve('qfKey', 'String'));
}
if (method_exists($response, 'confirm')) {
if ($redirectUrl && method_exists($response, 'confirm')) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton The !$redirectUrl was added by you recently, but I'm not sure I understand why. In sagepay case at least it needs to have a redirectUrl in order to call those functions as that redirect URL is passed back to sagepay via the IPN handler. Uniquely(?) for sagepay the confirm/error/invalid methods do not return as it is up to the sagepay website to initiate the redirect.

if (empty($value)) {
unset($cardFields[$name]);
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton Is this something that's acceptable for all payment processors?

@eileenmcnaughton
Copy link
Owner

@mattwire did composer run cleanly for you? I got

Loading composer repositories with package infoReading composer.json of omnipay/tests (master) Skipped branch master, Could not parse version constraint 4^: Invalid version string "4^"

Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.

Problem 1
- omnipay/omnipay 2.3.x-dev requires omnipay/sagepay ~2.0 -> satisfiable by omnipay/sagepay[2.0.x-dev].
- omnipay/omnipay 2.3.x-dev requires omnipay/sagepay ~2.0 -> satisfiable by omnipay/sagepay[2.0.x-dev].
- omnipay/omnipay 2.3.x-dev requires omnipay/sagepay ~2.0 -> satisfiable by omnipay/sagepay[2.0.x-dev].
- Can only install one of: omnipay/sagepay[2.0.x-dev, dev-2.5.1-mjw].
- Can only install one of: omnipay/sagepay[2.0.x-dev, dev-2.5.1-mjw].
- Installation request for omnipay/sagepay dev-2.5.1-mjw -> satisfiable by omnipay/sagepay[dev-2.5.1-mjw].
- Installation request for omnipay/omnipay (locked at 2.3.x-dev, required as ~2.0) -> satisfiable by omnipay/omnipay[2.3.x-dev].

@mattwire mattwire changed the title WIP Sagepay support Sagepay support Nov 2, 2018
@jamienovick
Copy link

@mattwire - ooh wish I'd seen this before - this may solve a few sagepay related issues we are having. How are you getting on with this - anything we could do to help get it over the line? Do you know if it covers all CiviCRM use cases including backend transactions / recurring etc?

@mattwire
Copy link
Contributor Author

@jamienovick It's in use on quite a few sites. It supports everything the omnipay extension supports, as it's a "redirect offsite" type processor you can't do backend stuff. If you'd like to help see this in the main extension (most of it already is) you could get some resource to look at the outstanding changes and the history of comments behind it (most is to do with the redirect back to site after payment).

@mattwire
Copy link
Contributor Author

Closing in favour of #147

@mattwire mattwire closed this Jan 27, 2020
@mattwire mattwire deleted the sagepay branch March 10, 2021 14:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants