Code Monkey home page Code Monkey logo

sagepay-integration's Introduction

Build Status Latest Stable Version Total Downloads Latest Unstable Version License

Note

This package is deprecated. Please now use academe/opayo-pi

Sage Pay Integration PSR-7 Message REST API Library

This package provides the data models for the Sage Pay Integration payment gateway, also known as the Sage Pay Pi or REST API. It does not provide the transport mechanism, so you can use what you like for that, for example Guzzle, curl or another PSR-7 library.

You can use this library as a PSR-7 message generator/consumer, or go a level down and handle all the data through arrays - both are supported.

Package Development

The Sage Pay Integration payment gateway is a RESTful API run by by Sage Pay. You can apply for an account here (my partner link).

This master branch contains a lot of reorganisation and renaming of classes compared to the previous PSR7 branch. The new class names should hopefully link more closely to the RESTful nature of the API. The PSR7 branch is now in maintenance mode only, and won't have any major changes - just bugfixes if they are reported. The aim is to release on the master branch as soon as a demo (and some units tests) are up and running.

The aim is for this package to support ALL functionality that the gateway supports, keeping up with changes quickly.

Want to Help?

Issues, comments, suggestions and PRs are all welcome. So far as I know, this is the first API for the Sage Pay Integration REST API, so do get involved, as there is a lot of work to do.

Tests need to be written. I can extend tests, but have not got to the stage where I can set up a test framework from scratch.

More examples of how to handle errors is also needed. Exceptions can be raised in many places. Some exceptions are issues at the remote end, some fatal authentication errors, and some just relate to validation errors on the payment form, needing the user to fix their details. Temporary tokens expire over a period and after a small number of uses, so those all need to be caught and the user taken back to the relevant place in the protocal without losing anything they have entered so far (that has not expired).

Overview; How to use

Note that this example code deals only with using the gateway from the back-end. There is a JavaScript front-end too, with hooks to deal with expired session keys and card tokens. This library does provide support for the front end though, and this is noted where relevant.

Installation

Get the latest release:

composer.phar require academe/sagepaymsg

Until this library has been released to packagist, include the VCS in composer.json:

"repositories": [
    {
        "type": "vcs",
        "url": "https://github.com/academe/SagePay-Integration.git"
    }
]

Create a Session Key

The CreateSessionKey message has had PSR-7 support added, and can be used like this:

// composer require guzzlehttp/guzzle
// This will bring in guzzle/psr7 too, which is what we will use.

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException; // Or your favourite PSR-18 client
use Academe\SagePay\Psr7\Model\Auth;
use Academe\SagePay\Psr7\Model\Endpoint;
use Academe\SagePay\Psr7\Request\CreateSessionKey;
use Academe\SagePay\Psr7\Factory;
use Academe\SagePay\Psr7\Request\CreateCardIdentifier;
use Academe\SagePay\Psr7\Factory\ResponseFactory;

// Set up authentication details object.

$auth = new Auth('vendor-name', 'your-key', 'your-password');

// Also the endpoint.
// This one is set as the test API endpoint.

$endpoint = new Endpoint(Endpoint::MODE_TEST); // or Endpoint::MODE_LIVE

// Request object to construct the session key message.

$keyRequest = new CreateSessionKey($endpoint, $auth);

// PSR-7 HTTP client to send this message.

$client = new Client();

// You should turn HTTP error exceptions off so that this package can handle all HTTP return codes.

$client = new Client();

// Send the PSR-7 message. Note *everything* needed is in this message.
// The message will be generated by guzzle/psr7 or zendframework/zend-diactoros, with discovery
// on which is installed. You can explictly create the PSR-7 factory instead and pass that in
// as a third parameter when creating Request\CreateSessionKey.

$keyResponse = $client->sendRequest($keyRequest->message());

// Capture the result in our local response model.
// Use the ResponseFactory to automatically choose the correct message class.

$sessionKey = ResponseFactory::fromHttpResponse($keyResponse);

// If an error is indicated, then you will be returned an ErrorCollection instead
// of the session key. Look into that to diagnose the problem.

if ($sessionKey->isError()) {
    // $session_key will be Response\ErrorCollection
    var_dump($sessionKey->first());
    exit; // (Obviously just a test script!)
}

// The result we want:

echo "Session key is: " . $sessionKey->getMerchantSessionKey();

Get a Card Identifier

The Card Identifier (a temporary, tokenised card detail) can be created using the equally temporary session key.

Normally it would be created on the front end, using an AJAX request from your browser, so the card details would never touch your application. For testing and development, the card details can be sent from your test script, emulating the front end.

use Academe\SagePay\Psr7\Request\CreateCardIdentifier;

// Create a card indentifier on the API.
// Note the MMYY order is most often used for GB gateways like Sage Pay. Many European
// gateways tend to go MSN first, i.e. YYMM, but not here.
// $endpoint, $auth and $session_key from before:

$cardIdentifierRequest = new CreateCardIdentifier(
    $endpoint, $auth, $sessionKey,
    'Fred', '4929000000006', '1220', '123' // name, card, MMYY, CVV
);

// Send the PSR-7 message.
// The same error handling as shown earlier can be used.

$cardIdentifierResponse = $client->sendRequest($cardIdentifierRequest->message());

// Grab the result as a local model.
// If all is well, we will have a Resposne\CardIdentifier that will be valid for use
// for the next 400 seconds.

$cardIdentifier = Factory\ResponseFactory::fromHttpResponse($cardIdentifierResponse);

// Again, an ErrorCollection will be returned in the event of an error:

if ($cardIdentifier->isError()) {
    // $session_key will be Response\ErrorCollection
    var_dump($cardIdentifier->first());
    exit; // Don't do this in production.
}

// When the card is stored at the front end browser only, the following three
// items will be posted back to your application.

echo "Card identifier = " . $cardIdentifier->getCardIdentifier();
echo "Card type = " . $cardIdentifier->getCardType(); // e.g. Visa

// This card identifier will expire at the given time. Do note that this
// will be the timestamp at the Sage Pay server, not locally. You may be
// better off just starting your own 400 second timer here.

var_dump($cardIdentifier->getExpiry()); // DateTime object.

At this point the card details are sane and have been saved in the remote API. Nothing has been checked against the bank, so we have no idea yet if these details will be authenticated or not.

What is a mystery to me is just why the card identifier is needed at all. The session key is only valid for one set of card details, so the session key should be all the Sage Pay needs to know to access those card details when the final purchase is requested. But no, this additional "card identifier" also needs to be sent to the gateway.

The merchantSessionKey identifies a short-lived storage area in the gateway for passing the card details from client to gateway. The cardIdentifier then identifies a single card within the storage area.

Submit a Transaction

A transaction can be initiated using the card identifier.

use Academe\SagePay\Psr7\Money;
use Academe\SagePay\Psr7\PaymentMethod;
use Academe\SagePay\Psr7\Request\CreatePayment;
use Academe\SagePay\Psr7\Request\Model\SingleUseCard;
use Academe\SagePay\Psr7\Money\Amount;
use Academe\SagePay\Psr7\Request\Model\Person;
use Academe\SagePay\Psr7\Request\Model\Address;
use Academe\SagePay\Psr7\Money\MoneyAmount;
use Money\Money as MoneyPhp;

// We need a billing address.
// Sage Pay has many mandatory fields that many gateways leave as optional.
// Sage Pay also has strict validation on these fields, so at the front end
// they must be presented to the user so they can modify the details if
// submission fails validation.

$billingAddress = Address::fromData([
    'address1' => 'address one',
    'postalCode' => 'NE26',
    'city' => 'Whitley',
    'state' => 'AL',
    'country' => 'US',
]);

// We have a customer to bill.

$customer = new Person(
    'Bill Firstname',
    'Bill Lastname',
    '[email protected]',
    '+44 191 12345678'
);

// We have an amount to bill.
// This example is £9.99 (999 pennies).

$amount = Amount::GBP()->withMinorUnit(999);

// Or better to use the moneyphp/money package:

$amount = new MoneyAmount(MoneyPhp::GBP(999));

// We have a card to charge (we get the session key and captured the card identifier earlier).
// See below for details of the various card request objects.

$card = new SingleUseCard($session_key, $card_identifier);

// If you want the card to be reusable, then set its "save" flag:

$card = $card->withSave();

// Put it all together into a payment transaction.

$paymentRequest = new CreatePayment(
    $endpoint,
    $auth,
    $card,
    'MyVendorTxCode-' . rand(10000000, 99999999), // This will be your local unique transaction ID.
    $amount,
    'My Purchase Description',
    $billingAddress,
    $customer,
    null, // Optional shipping address
    null, // Optional shipping recipient
    [
        // Don't use 3DSecure this time.
        'Apply3DSecure' => CreatePayment::APPLY_3D_SECURE_DISABLE,
        // Or force 3D Secure.
        'Apply3DSecure' => CreatePayment::APPLY_3D_SECURE_FORCE,
        // There are other options available.
        'ApplyAvsCvcCheck' => CreatePayment::APPLY_AVS_CVC_CHECK_FORCE
    ]
);

// Send it to Sage Pay.

$paymentResponse = $client->sendRequest($paymentRequest->message());

// Assuming we got no exceptions, extract the response details.

$payment = ResponseFactory::fromHttpResponse($paymentResponse);

// Again, an ErrorCollection will be returned in the event of an error.
if ($payment->isError()) {
    // $payment_response will be Response\ErrorCollection
    var_dump($payment->first());
    exit;
}

if ($payment->isRedirect()) {
    // If the result is "3dAuth" then we will need to send the user off to do their 3D Secure
    // authorisation (more about that process in a bit).
    // A status of "Ok" means the transaction was successful.
    // A number of validation errors can be captured and linked to specific submitted
    // fields (more about that in a bit too).
    // In future gateway releases there may be other reasons to redirect, such as PayPal
    // authorisation.
    // ...
}

// Statuses are listed in `AbstractTransaction` and can be obtained as an array using the static
// helper method:
// AbstractTransaction::constantList('STATUS')

echo "Final status is " . $payment->getStatus();

if ($payment->isSuccess()) {
    // Payment is successfully authorised.
    // Store everything, then tell the user they have paid.
}

Fetch a Transaction Result Again

Given the TransactionId, you can fetch the transaction details. If the transaction was successful, then it will be available immediately. If a 3D Secure action was needed, then the 3D Secure results need to be sent to Sage Pay before you can fetch the transaction. Either way, this is how you do it:

// Prepare the message.

$transaction_result = new Request\FetchTransaction(
    $endpoint,
    $auth,
    $transaction_response->getTransactionId() // From earlier
);

// Send it to Sage Pay.

$response = $client->sendRequest($transaction_result->message());

// Assuming no exceptions, this gives you the payment or repeat payment record.
// But do check for errors in the usual way (i.e. you could get an error collection here).

$fetched_transaction = ResponseFactory::fromHttpResponse($response);

Repeat Payments

A previous transaction can be used as a base for a repeat payment. You can amend the shipping details and the amount (with no limit) but not the payee details or address.

use Academe\SagePay\Psr7\Request\CreateRepeatPayment;

$repeat_payment = new CreateRepeatPayment(
    $endpoint,
    $auth,
    $previous_transaction_id, // The previous payment to take card details from.
    'MyVendorTxCode-' . rand(10000000, 99999999), // This will be your local unique transaction ID.
    $amount, // Not limited by the original amount.
    'My Repeat Purchase Description',
    null, // Optional shipping address
    null // Optional shipping recipient
);

All other options remain the same as for the original transaction (though it does appear that giftAid can now be set in the API).

Using 3D Secure

Now, if you want to use 3D Secure (and you really should) then we have a callback to deal with.

To turn on 3D Secure, use the appropriate option when sending the payment:

$payment = new CreatePayment(
    ...
    [
        // Also available: APPLY_3D_SECURE_USEMSPSETTING and APPLY_3D_SECURE_FORCEIGNORINGRULES
        'Apply3DSecure' => CreatePayment::APPLY_3D_SECURE_FORCE,
    ]
);

3D Secure Redirect

The result of the transaction, assuming all is otherwise fine, will be a Secure3DRedirect object. This message will return true for isRedirect(). Given this, a POST redirection is needed. Note also that even if the card details were invalid, a 3D Secure redirect may still be returned. It is not clear why the banks do this, but you just have to go with with it.

This minimal form will demonstrate how the redirect is done:

// $transaction_response is the message we get back after sending the payment request.

if ($transactionResponse->isRedirect()) {
    // This is the bank URL that Sage Pay wants us to send the user to.

    $url = $transactionResponse->getAcsUrl();

    // This is where the bank will return the user when they are finished there.
    // It needs to be an SSL URL to avoid browser errors. That is a consequence of
    // the way the banks do the redirect back to the merchant siteusing POST and not GET,
    // and something we cannot control.

    $termUrl = 'https://example.com/your-3dsecure-result-handler-post-path/';

    // $md is optional and is usually a key to help find the transaction in storage.
    // For demo, we will just send the vendorTxCode here, but you should avoid exposing
    // that value in a real site. You could leave it unused and just store the vendorTxCode
    // in the session, since it will always only be used when the user session is available
    // (i.e. all callbacks are done through the user's browser).

    $md = $transactionResponse->getTransactionId();

    // Based on the 3D Secure redirect message, our callback URL and our optional MD,
    // we can now get all the POST fields to perform the redirect:

    $paRequestFields = $transactionResponse->getPaRequestFields($termUrl, $md);

    // All these fields will normally be hidden form items and the form would auto-submit
    // using JavaScript. In this example we display the fields and don't auto-submit, so
    // you can se what is happening:

    echo "<p>Do 3DSecure</p>";
    echo "<form method='post' action='$url'>";
    foreach($paRequestFields as $field_name => $field_value) {
        echo "<p>$field_name <input type='text' name='$field_name' value='$field_value' /></p>";
    }
    echo "<button type='submit'>Click here if not redirected in five seconds</button>";
    echo "</form>";

    // Exit in the appropriate way for your application or framework.
    exit;
}

The above example does not take into account how you would show the 3D Secure form in an iframe instead of inline. That is out of scope for this simple description, for now at least. Two main things need to be considered when using an iframe: 1) the above form must target the iframe by name; and 2) on return to the $termUrl, the page must break itself out of the iframe. That's the absolute essentials.

This form will then take the user off to the 3D Secure password page. For Sage Pay testing, use the code password to get a successful response when you reach the test 3D Secure form.

Now you need to handle the return from the bank. Using Diactoros (and now Guzzle) you can catch the return message as a PSR-7 ServerRequest like this:

use Academe\SagePay\Psr7\ServerRequest\Secure3DAcs;

$serverRequest = \GuzzleHttp\Psr7\ServerRequest::fromGlobals();
// or if using a framework that supplies a PSR-7 server request, just use that.

// isRequest() is just a sanity check before diving in with assumptions about the
// incoming request.

if (Secure3DAcs::isRequest($serverRequest->getBody()))
    // Yeah, we got a 3d Secure server request coming at us. Process it here.

    $secure3dServerRequest = new Secure3DAcs($serverRequest);
    ...
}

or

use Academe\SagePay\Psr7\ServerRequest\Secure3DAcs;

if (Secure3DAcs::isRequest($_POST)) {
    $secure3dServerRequest = Secure3DAcs::fromData($_POST);
    ...
}

Both will work fine, but it's just about what works best for your framework and application.

Handling the 3D Secure result involves two steps:

  1. Passing the result to Sage Pay to get the 3D Secure state (CAUTION: see note below).
  2. Fetching the final transaction result from Sage Pay.
    use Academe\SagePay\Psr7\Request\CreateSecure3D;

    $request = new CreateSecure3D(
        $endpoint,
        $auth,
        $secure3dServerRequest,
        // Include the transaction ID.
        // For this demo we sent that as `MD` data rather than storing it in the session.
        // The transaction ID will generally be in the session; putting it in MD exposes it
        // to the end user, so don't do this unless use a nonce!
        $secure3dServerRequest->getMD()
    );

    // Send to Sage Pay and get the final 3D Secure result.

    $response = $client->send($request->message());
    $secure3dResponse = ResponseFactory::fromHttpResponse($response);

    // This will be the result. We are looking for `Authenticated` or similar.
    //
    // NOTE: the result of the 3D Secure verification here is NOT safe to act on.
    // I have found that on live, it is possible for the card to totally fail
    // authentication, while the 3D Secure result returns `Authenticated` here.
    // This is a decision the bank mnakes. They may skip the 3D Secure and mark
    // it as "Authenticated" at their own risk. Just log this information.
    // Instead, you MUST fetch the remote transaction from the gateway to find
    // the real state of both the 3D Secure check and the card authentication
    // checks.

    echo $secure3dResponse->getStatus();

Final Transaction After 3D Secure

Whether 3D Secure passed or not, get the transaction. However - do not get it too soon. The test instance of Sage Pay has a slight delay between getting the 3D Secure result and being able to fetch the transaction. It is safer just to sleep for one second at this time, which is an arbitrary period but seems to work for now. A better method would be to try immediately, then if you get a 404, back off for a short time and try again, and maybe once more if necessary. This is supposed to have been fixed in the gateway several times, but still gets occasionally reported as still being an issue.

    // Give the gateway some time to get its syncs in order.

    sleep(1);

    // Fetch the transaction with full details.

    $transactionResult = new FetchTransaction(
        $endpoint,
        $auth,
        // transaction ID would normally be in the session, as described above, but we put it
        // into the MD for this demo.
        $secure3dServerRequest->getMD()
    );

    // Send the request for the transaction to Sage Pay.

    $response = $client->sendRequest($transactionResult->message());

    // We should now have the payment, repeat payment, or an error collection.

    $transactionFetch = ResponseFactory::fromHttpResponse($response);

    // We should now have the final results.
    // The transaction data is all [described in the docs](https://test.sagepay.com/documentation/#transactions).

    echo json_encode($transactionFetch);

Payment Methods

At this time, Sage Pay Pi supports just card payment types. However, there are three different types of card object:

  1. SingleUseCard - The fist time a card is used. It has been tokenised and will be held against the merchant session key for 400 seconds before being discarded.
  2. ReusableCard - A card that has been saved and so is reusable. Use this for non-interaractive payments when no CVV is being used.
  3. ReusableCvvCard - A card that has been saved and so is reusable, and has been linked to a CVV and merchant session. Use this for interactive reuse of a card, where the user is being asked to supply their CVV for additional security, but otherwise do not need to reenter all their card details. The CVV is (normally) linked to the card and the merchant session on the client side, and so will remain active for a limited time (400 seconds).

The ReusableCard does not need a merchant session key. ReusableCvvCard does require a merchant session key and a call to link the session key + card identifier + CVV together (preferably on the client side, but can be done server-side if appropriately PCI accredited or while testing).

A CVV can be linked to a reusable card with the LinkSecurityCode message:

use Academe\SagePay\Psr7\Request\LinkSecurityCode;

$securityCode = new LinkSecurityCode(
    $endpoint,
    $auth,
    $sessionKey,
    $cardIdentifier,
    '123' // The CVV obtained from the user.
);

// Send the message to create the link.
// The result will be a `Response\NoContent` if all is well.

$securityCodeResponse = ResponseFactory::fromHttpResponse(
    $client->sendRequest($securityCode->message())
);

// Should check for errors here:

if ($securityCodeResponse->isError()) {...}

To save a reusable card, take the PaymentMethod from a successful payment. Note: it is not possible at this time to set up a reusable card without making a payment. That is a restriction of the gateway. Some gateways will allow you to create zero-amount payments just to authenticate and set up a reusable card, but not here.

...

// Get the transaction response.

$transactionResponse = ResponseFactory::fromHttpResponse($response);

// Get the card. Only cards are supported as Payment Method at this time,
// though that is likely to change when PayPal support is rolled out.

$card = $transactionResponse->getPaymentMethod();

// If it is reusable, then it can be serialised for storage:

if ($card->isReusable()) {
    // Also can use getData() if you want the data without being serialised.
    $serialisedCard = json_encode($card);
}

// In a later payment, the card can be reused:

$card = ReusableCard::fromData(json_decode($serialisedCard));

// Or more explicitly:

$card = new ReusableCard($cardIdentifier);

// Or if being linked to a freshly-entered CVV:

$card = new ReusableCard($merchantSessionKey, $cardIdentifier);

sagepay-integration's People

Contributors

boffey avatar fireatwill avatar judgej avatar ozdemirburak avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

sagepay-integration's Issues

Response message factory

Being able to throw a PSR-7 response into a factory, and getting back the appropriate response object would be great, and take away some of the guesswork.

Not sure how feasible this will be. We have the HTTP response code, and the body contents to inspect, so it should all be possible. Guzzle would have its exceptions disabled (which it raises on a non-2xx response) so a PSR-7 response message would always be available for parsing.

Support saved cards

The way the gateway works, or is documented, can be a little unclear (to me!). It shows a payment being made with a "card" object. That card object has been generated using a merchant session key to create a temporary tokenised card.

Now, a saved card can also be used, but a saved card is not generated using a merchantSessionKey. It happens to have the save ID as the one that was (checked and confirmed) but is not linked to the session key. But the documentation does not give this as an example, so it is not clear how it works, since it states that the merchantSessionKey is mandatory, when clear;y it is not (probably) unless you choose to relink a security code with it.

I think the issue is that "cardIdentifier" is overused. It represents both temporary card details with a 400 second lifetime, and saved card details that have been verified and stored in the gateway for future use. I suspect these are two very different resources.

Some general restructuring

A few assumptions made now are being revised as the API progresses.

One big change is that some models have both a Request and a Response form (e.g. Card). It makes sense to create these under the relevant Request or Response directories (in Model subdirectories since these are not raw PSR-7 messages).

In addition, the way error handling works is now a little clearer. A transaction failure is not an error condition if the transaction fails to authenticate for remote reasons. It is a valid response that happens not to be successful. A missing address field, for example, is an error, since the transaction could not be progressed.

Support clientMessage

The list of validation error codes now includes a clientMessage field that is suitable for presenting to the end user (presumably only if they are English, until a language/locale setting is introduced in the transaction submit).

Remove the RecurringIndicator

This was in an early version of the API only, and quietly removed in a later version. It is no longer needed and does nothing.

4020: Information received from an Invalid IP address.

Just started getting this message from the API when requesting a payment.

Not sure what has changed - same IP, same code, same message details as was working a week ago. All POST details sent to Sage Pay to look at.

Support security-code action

This action links a saved cardIdentifier to a new merchantSessionKey with a freshly entered securityCode (CVV) to perform a 3D Secure check when re-using a saved card token, for extra security.

What strikes me as odd if that the examples given show how it would be used from the server side only, which you would want to avoid for PCI reasons. With the right PCI accreditation it can be done, so this library should support it, but in most cases it would be a client-side call only.

Fix API Version Number for any release or branch

The initial thought was a package that can use any API version and adapt as they are released. Starting to think otherwise - it should be fixed in a release. Multiple branches can be maintained if it is important to do so.

The release major version number should track the API version number (if this is feasible) though that does break semver somewhat for this package, but we'll see how it goes.

The API version number could affect any part of this package, from field validation, object construction, to what services are supported. So it is not just something linked to Auth and the URL endpoints only.

Expand supported currencies

Not sure where it came from, but the Currency object is limited to seven possible currencies. I am not sure there is any real limit though. So long as a linked merchant account supports a currency, then the gateway should support it.

I still have niggling doubt though - those seven currencies did come from somewhere. Will find out for sure, and then pull in a list of currencies as necessary.

Support "Repeat" transactions

This is being released on test today, hopefully with a documentation update to follow quickly. This is the first new transaction type that the REST API is offering after the simple payment transaction.

Repeat transactions support service subscriptions and are great for SAAS products. It could sit alongside Stripe on Laravel Spark, for example.

Remove built-in autoloader

This was added when considering running this package in a non-composer environment. Now it has a dependency on shrikeh/teapot, which in turn requires squizlabs/php_codesniffer, use without composer is going to be a lot more difficult.

We should either remove the built-in autoloader, or remove composer library dependencies. Or maybe extend the built-in autoloader to autoload Teapot if it is installed alongside this package? It does not look like squizlabs/php_codesniffer is needed by any of the Teapot interfaces that we use.

Create helper class

Messages and some models inherit abstract messages just for access to some helper methods. These generic helpers would be better in a class of their own.

Separate person details for Customer, Billing and Shipping

At the moment, the BillingDetails contains the billingAddress and the Customer details (name, phone, email).

In reality, the customer is not necessarily the billing person. The customer is logged into a shop and can make a purchase, but the billing person may be another party with the credit card. This needs a little more separation, so that the customer name/email/phone can be overridden from what is given to the billing details object.

Test UTF-8, more

Sending UTF-8 extended characters, they appear in the testing panel as question marks (?). It is not clear where that conversion is taking place - at the gateway, or within the Guzzle client.

The gateway test panel is a shameless ISO8859-1, but UTF-8 encoded characters that fit into the extended ASCII set are being correctly converted somewhere. So Iñtërnâtiônàlizætiøn as UTF-8 is displaying as Iñtërnâtiônàlizætiøn in the testing panel. But アイウエオカキクケコサシスセソタチツテ goes over as ???????????????????. The character counts are correct, so something is making a good attempt at converting the UTF encoding, but is using "?" as the out-of-bounds replacement character.

Create response message factory

Instead of handling which message to send the response into in the application, just send it into the factory and get the appropriate response message back. It would be a kind of "auto-detect" that would return a message, which could include a collection of errors.

A general "state" or "message type" property of the response will be useful to make high-level decisions.

This could also apply to ServerResponse messages, with one possible state being "nothing of relevance here".

This would make writing the wrapper code much easier. pulling more of the sequence detection logic into this package.

Go PSR-7

A new branch PSR-7 has been created to manage this.

From the start, this package generates and accepts arrays of data, and provides additional HTTP details (GET/POST/auh) as separate properties. This was so it could stick to PHP5.4 and be integrated in standalone plugins for various non-composer PHP packages.

Now let's branch and got PSR-7 all-out. The idea is that this package accepts and generates PSR-7 messages. These messages can be put "onto the wire" using various HTTP clients, such as Guzzle 6+.

Use SagePay field names in validation exceptions

Not sure how this would work, but it's worth looking at.

Each field submitted to SagePay has a unique name. Those names are used to link any validation errors to the correct fields. That's find for data that has been submitted to SagePay.

Data submitted to the user needs to be put into models before it can be sent to SagePay. Sometimes the validation could fail at that point, such as a missing postcode with a country other than "IE" put into an Address object. In this case an exception is thrown. Ideally we would be able to catch the field name from that exception, e.g. billing.postalCode. These fields can map the exception to errors in the relevant fields using the same logic as for mapping SagePay validation errors.

It's a thought anyway, and I can see this type of thing already being done in the likes of Guzzle, where a HTTP exception prevents the response being set, but the response object is still available in the exception handler, somehow attached to the exception itself.

Better JS wrapper

Create a wrapper script for sagepay.js to cater for a few use-cases a bit more elegantly. It should not make any assumptions about:

  • Selectors for the form.
  • HTML layout of the form.
  • Method of displaying messages connected to form items.
  • Other items (not related to card details) that may be in the form.

It should be able to:

  • Handle form item errors.
  • Handle non-form item errors.
  • Handle an expired session token.
  • Handle form submission on successfully obtaining a form token.

This would be ideally implemented using callbacks, so the application can decide what to do about each of these circumstances.

It can be based on jQuery, since that is already a dependency of sagepay.js (in the beta v1 release at least, maybe not long-term).

Support single error being returned with HTTP 422 result

The 422 return code indicates validation errors on some data. During testing, it seems to be a bit hit-or-miss as to whether this code returns a single error, or an array of multiple errors, given that more than one field could be failing validation.

I'm not going to question why, oh, why, but the ErrorCollection::fromData() method needs to be able to recognise which it is receiving and handle it appropriately.

  • If it receives an array with numeric indices, then the result will be an array of errors.
  • If it receives an array of non-numeric indices, then assume it has been given a single error.

Test and updates for V1 API

The API is out of beta, as of today (on the test instance at least). The change log is "Updated getting started and guides sections to include drop-in checkout", so there may not be any changes to the back end at least. But we still need to check it all out. Perhaps time to start adding some tests.

Refresh merchantSessionKey when used multiple times

Noticed this when testing with the demo code.

When trying to submit, the sagepay.js script catches your form submit and attempts to get a token for the card details entered. If the token cannot be fetched for any reason - invalid characters, type in CC number or date, missing CVV2 etc, then the form is not submitted to the server.

Each time an attempt to get a card token is made, the merchantSessionKey loses of of its lives. It starts with only three lives (three attempts as use) and a lifetime period of 400 seconds. Once either of these are exceeded, SagePay responds with a 401. The scripts in the page need to recognise this, and refresh the merchantSessionKey.

Whether that refresh happens through AJAX (fetching a new value from the server) or by allowing the whole form to be submitted and re-presented with a new merchantSessionKey is unclear. Once the card identifier is successfully obtained, it will last for up to 400 seconds and the payment form can be submitted multiple times. The card details of the form, when represented, should probably be hidden and disabled, and shown only if the user wishes to change to a different card, or the card token has expired or been used too many times.

The documentation only lists a card token (cardIdentifier) as lasting 400 seconds, and does not list a maximum number of times it can be used to submit the payment request. Multiple submissions may be needed to get the address details into a valid state, for example.

Anyway - we need to get a better demo together that brings all these expiring tokens together and handles them appropriately.

MDX - what's that then?

The callback after the user enters their 3D Secure password includes an MD field and an MDX field. It is not clear what MDX is, why it is needed, and how it is used. Do we need to capture it, and if so, why?

Create Person class

A person always has example one first name and one last name. Put them together in a class. You know it makes sense.

Replace Address::fromArray()

The array is too strict. Replace it with a fromData method that uses the helper function to make things a little simpler to use.

Handle Card Tokens

This is not yet provided by the v1 API, but some background:

Either as a transaction on its own (without taking a payment) or on the back of a payment transaction, SagePay can be asked to store the card details and provide a token for it that the merchant site can store. Future visits to the shop will allow purchases to be made using the token in lieu of the credit card. The user just needs to enter their CV2 code.

For this to work, the following is needed in the API:

  • A flag to tell SagePay a token is wanted (CreateToken = 1) in a payment transaction.
  • A transaction type that can be used just to receive a token ("registration").
  • A token returned from a payment transaction asking for one.
  • A token payment type to be used in transactions instead of the card detail.

On that last note, I suspect the API will accept a token and a CV2 to generate a card identifier, which will then carry on as though a full credit card had been entered. Or it may be totally different - we'll have to see.

Serialisation for choice objects

Some objects are going to be needed over a number of page loads. For example, the SessionKeyResponse object can be used to get a card identifier token for up to three attempts. Once obtained, the CardIdentifierResponse will be valid for 200 seconds, so can be used to submit a form multiple times (catching validation errors each time). These objects will need to be serialisable so they can be saved and reconstructed on each page load.

Add void support

This uses an "instructions" endpoint, under the transaction endpoint, so differs from the transaction endpoints in that respect.

More appropriate name for this API?

It is not entirely clear what this API is called. It operates as a RESTful API, and can operate entirely direct server-to-server.

There is also a sagepay.js script that allows a temporary card token to be generated from the user's browser. That would be the most common use-case for this API, and lowers the PCI requirements considerably. However, using the JavaScript on the front end is not mandatory nor a necessity.

Support endpoint to retrieve an existing transaction

New endpoint https://test.sagepay.com/api/v1/transactions/<transactionId> to get a transaction.

It may also be worth trying GET https://test.sagepay.com/api/v1/transactions to see if transaction listings are available too.

Handle 3D Secure

If a 3D Secure redirect is needed, SagePay will return this response to the transaction request:

array(3) {
  ["statusCode"]=>
  string(4) "2007"
  ["statusDetail"]=>
  string(70) "Please redirect your customer to the ACSURL, passing the MD and PaReq."
  ["status"]=>
  string(6) "3DAuth"
}

Status code 2007 is not documented for this API (though is listed in SagePay's master list of error codes), and it is not clear where the URL comes from. The HTTP response is 200.

Expand sequence diagram

Sage Pay provide a sequence diagram showing the success path of a transaction. This is great for when things work first time.

What the diagram does not show, is the points and actions when session tokens and card identifiers expire (by time or by number of uses). In these cases the sequence is taken back to an earlier stage (preserving all user-entered data captured so far) so that tokens and identifiers can be renewed. At some points this will need data to be renetered by the user, in other places it won't.

Handle Surcharges (or maybe not?)

Making a big assumption, I suspect surcharges will be handled very differently.

With Sage Pay Server, the rules for adding surcharges, based on which payment type the user chooses, is sent to Sage Pay so that the calculation can be done there. The merchant site has no idea what card is used, and so what the final charge is, until the result of the transaction comes back. It is likely to differ from the base charge if surcharges are used.

Now, with Pi (Sage Pay Integration) the merchant site gets to know what payment type was chosen before the transaction for a payment or authorisation is made. So the surcharges can now be handled entirely on the merchant site. This can even go to the extent that JavaScript on the payment form can be used to present the surcharge to the user before the payment is submitted, so the user has a change to back out or choose another payment method.

So given that, why would Sage Pay Pi need to handle surcharges at all? I don't think they will (but I will ask).

A method in this package to register surcharges and match against card types would be handy. Or maybe just leave it up to the payment wrapper package to implement that? Any thoughts? I'm going to sleep on it. Remember, this package handles the data, the messages, and a certain amount of the logic, but how that is put together into a user flow is for a wrapper package or plugin to take care of, because there are just too many different ways this API may be used.

3D Secure object structure fix

An earlier version of the spec listed the 3D Secure status response wrapped in a 3DSecure element. This does not happen in the final 3D Secure status response, which now looks like this:

{
    "status" : "Authenticated"
}

The documentation was recently changed to correct this (though did not get a changelog entry).

This kind makes recognising objects a little more difficult, because "status" on its own has absolutely no context. Will need to look at whether any of the request details can be used as a part of the context when converting a PSR-7 response message into the appropriate class.

securityCode is optional

18-02-2016 BETA Made securityCode optional when tokenising the card details.

When fetching a card token, the security code can be omitted.

Map Sage Pay Direct error codes to properties

CC validation at the REST API level provides error messages that include the "property" field that tells you which field is in error. An example is cardDetails.cardholderName which may come with a message to indicate it contains invalid characters.

Many errors are generated by the underlying Sage Pay Direct, and that does NOT provide metadata to indicate which field is in error. However, it does provide a status code that can be mapped to a property.

The status codes we know are listed here: https://github.com/academe/SagePay/blob/master/src/Academe/SagePay/Metadata/error-codes.tsv (there are some new ones 2009-2019 that need to be added, and are not even in Sage Pay's online database of error codes at time of writing).

For example error code 5055 has the message "A Postcode field contains invalid characters." From this we can map to a property billingAddress.postalCode. That is the property name, in dot-notation to indicate structure, that this field was sent to Sage Pay in the Request\Payment message. This is not an official name of this property, but does follow the convention that this REST API seems to implement, so far.

All "response" messages should instantiate with array data

The constructors for some of the response messages are ridiculously long lists of parameters. Let's take them all out and merge the fromData() method into the constructor. So ALL received messages are instantiated from an array of data.

Create state machine for a single-form payment

Okay, a state machine sounds a little fancy, but the idea is to map out all the states needed to be able to support payment in a single form.

The form would contain the auto-generated CC fields, and the personal details (name, address etc.). LOTS of things need to happen in the right order for this to work.

  • The CC form can only be displayed with a brand new session key.
  • If a new session key is generated then the CC form needs to be redisplayed.
  • If the session key or card identifier expire, then the CC form needs to be redisplayed.
  • Either may expire by time (so with no real warning) or if they have been submitted with invalid data too many times, e.g. invalid postcode format.
  • The whole form will not submit unless the CC data correctly tokenises.
  • If the address fields fail local validation, then the form needs to be represented for correction, but there is no need to reshow the CC fields again (the card identifier can be put into a hidden field).
  • The same applies for remote validation errors, to a point.
  • The card identifier may be found to have expired on the final transaction request, in which case the form needs to be shown again so that CC fields can re-entered for tokenisation. However, we want to keep the personal details fields that the user may have edited to get this far past the validation.

In short, one simple success path through, but MANY things that can expire or be found to be invalid at various stages, with jumps back to earlier stages in the path needed to pick up on those exceptions.

We also need to think hard about how JS validation of personal details would work, since it needs to work alongside the CC details validation. Perhaps the way to handle that would be a callback on successful tokenisation of the card to physically remove the CC form fields immediately so it won't be accidentally tokenised twice (the second time of which would fail).

Also note that the AJAX tokenisation may also discover the session key is expired or overused, and so need to fetch a new from the server to retry tokenisation.

It might make more sense as a spreadsheet. It's had me stumped for a long time. My solution at the moment is to have two separate forms on two pages - the first to collect the CC details and tokenise them (AJAX) and the second to submit the personal details. Once the user is past the first form, they can submit the second form as many times as needed to get through local validation, before eventually sending the payment request. It's not as smooth a UX as it should be as a consequence.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.