academe/opayo-pi

一个用于处理Opayo Pi(原Sage Pay Integration)支付网关的消息内容和结构的库

3.2.0 2024-03-23 20:03 UTC

This package is auto-updated.

Last update: 2024-08-23 21:11:08 UTC


README

Build Status Latest Stable Version Total Downloads Latest Unstable Version License

注意

由于composer包名的更改,此包是从academe/SagePay-Integration分支在新的仓库中分叉的。

Opayo Pi PSR-7 消息 REST API 库

此包为Opayo Pi(原Sage Pay Integration)支付网关提供数据模型。它不提供传输机制,因此您可以为此使用任何PSR-18客户端,例如Guzzle(7+或6+HTTPlug适配器)、curl或另一个PSR-7库。

您可以使用此库作为PSR-7消息生成/消费者,或者更深入一层,通过数组处理所有数据 - 这两种方式都受支持。

此包已更新为使用新的Elavon URL,这些URL将从2024年3月起成为强制要求。

包开发

Opayo Pi支付网关是由Elavon运行的RESTful API。

您可以在这里申请账户(合作伙伴链接)。

从v3.0.0版本开始,此包正在重新命名为Opayo,并获得新的composer名称和新基础命名空间。

现在的PSR7分支仅处于维护模式,不会进行任何重大更改 - 只有在报告错误时才会进行错误修复。目标是当演示(和一些单元测试)正常运行时,在master分支上发布。

目标是为此包提供后端支持,以实现网关支持的所有功能。

想要帮忙吗?

欢迎提出问题、评论、建议和PR。据我所知,这是第一个Opayo Pi REST API的API,所以请参与其中,因为还有很多工作要做。

需要编写测试。我可以扩展测试,但还没有达到从头开始设置测试框架的阶段。

还需要更多的错误处理示例。可以在许多地方引发异常。一些异常是远程端的问题,一些是致命的认证错误,还有一些只是与支付表单上的验证错误相关,需要用户修复他们的详细信息。临时令牌在一段时间后会过期,并在使用少量次数后,所以所有这些都需要捕获,并将用户返回到协议中的相关位置,而不会丢失他们之前输入的任何内容(尚未过期)。

概述;如何使用

请注意,此示例代码仅处理从后端使用网关的情况。还有一个JavaScript前端,其中包含处理过期会话密钥和卡令牌的钩子。虽然这个库也提供了前端支持,但相关部分已有说明。

安装

获取最新版本

composer.phar require academe/opayo-pi

在此库发布到packagist之前,请在composer.json中包含VCS

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

创建一个会话密钥

可以使用以下方式使用CreateSessionKey消息

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

use GuzzleHttp\Client; // Or your favourite PSR-18 client
use GuzzleHttp\Exception\ClientException;
use Academe\Opayo\Pi\Model\Auth;
use Academe\Opayo\Pi\Model\Endpoint;
use Academe\Opayo\Pi\Request\CreateSessionKey;
use Academe\Opayo\Pi\Factory;
use Academe\Opayo\Pi\Request\CreateCardIdentifier;
use Academe\Opayo\Pi\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 MODE_LIVE

// Request object to construct the session key message.

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

// PSR-18 HTTP client to send this message.
// If using Guzzle 6, then wrap it with an adapter such as HTTPlug,
// see https://docs.php-http.org/en/latest/clients/guzzle6-adapter.html

$client = new Client();

// Send the PSR-7 message to request a session key.
// 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);

// 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; // Better handling needed than this!
}

// The result we want:

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

会话密钥有效期为20分钟,允许您的网站前端使用Opayo的表单集成JavaScriptDrop-In Checkout来保存卡详细信息。这样,卡详细信息将保留在前端,而不会接近您的服务器。

获取卡标识符

可以使用同样临时的会话密钥创建卡标识符(这是一个临时、已令牌化的卡详细信息,其中卡实际上存储在Opayo)。

通常它会在前端创建,使用浏览器中的AJAX请求,这样卡详细信息就不会触及您的应用程序。对于测试和开发,可以从您的测试脚本发送卡详细信息,模拟前端,具体操作如下。

use Academe\Opayo\Pi\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 most significant number (MSN) first, i.e. YYMM.
// $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);

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

$cardIdentifier = 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 by 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.

此时,卡详细信息是合理的,并且已保存到远程API中。还没有与银行核对,所以我们还不知道这些详细信息是否将被验证。

对我来说是个谜的是,为什么需要卡标识符。会话密钥仅对一组卡详细信息有效,因此会话密钥应该是Sage Pay在请求最终购买时需要知道以访问这些卡详细信息的一切。但是不,这个额外的“卡标识符”也需要发送到网关。

merchantSessionKey标识了网关中用于从客户端传递卡详细信息到网关的短暂存储区域。然后,cardIdentifier标识了存储区域内的单个卡。

提交交易

可以使用卡标识符启动交易。

use Academe\Opayo\Pi\Money;
use Academe\Opayo\Pi\PaymentMethod;
use Academe\Opayo\Pi\Request\CreatePayment;
use Academe\Opayo\Pi\Request\Model\SingleUseCard;
use Academe\Opayo\Pi\Money\Amount;
use Academe\Opayo\Pi\Request\Model\Person;
use Academe\Opayo\Pi\Request\Model\Address;
use Academe\Opayo\Pi\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',
    'billing@example.com',
    '+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:
// You will also need to force 3D secure and set a credential type (see below)

$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);

// 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.
}

再次获取交易结果

给定交易ID,您可以获取交易详情。如果交易成功,则将立即可用。如果需要3D Secure操作,则必须在获取交易之前将3D Secure结果发送给Sage Pay。无论哪种方式,您都可以这样做

use Academe\Opayo\Pi\Request\FetchTransaction;

// Prepare the message.

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

// Send it to Sage Pay.

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

// 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).

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

重复支付

之前的交易可以用作重复支付的基座。您可以修改运输详情和金额(没有限制),但不能修改收款人详情或地址。

use Academe\Opayo\Pi\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
);

所有其他选项与原始交易相同(尽管现在似乎可以在API中设置giftAid)。

使用3D Secure

现在,如果您想使用3D Secure(您真的应该这样做,并且将在2022年被迫这样做)。

在发送付款时使用适当的选项来启用3D Secure

$paymentRequest = new CreatePayment(
    ...
    [
        // Also available: APPLY_3D_SECURE_USEMSPSETTING and APPLY_3D_SECURE_FORCEIGNORINGRULES
        'Apply3DSecure' => CreatePayment::APPLY_3D_SECURE_FORCE,
        // or set APPLY_3D_SECURE_USEMSPSETTING to control it from the MyOpayo panel.
    ]
);

3D Secure 版本 1

到2021年底,EC将逐步淘汰,到2022年3月,UK将淘汰。请使用版本2,这是强制性的。

3D Secure 版本 1 重定向

假设其他方面都正常,交易的结果将是一个Secure3DRedirect对象。此消息将为isRedirect()返回true。鉴于这一点,需要POST重定向。注意,即使卡详细信息无效,也可能返回3D Secure重定向。银行为何这样做尚不清楚,但您需要对此做好准备。

此最小表单将演示重定向是如何完成的

// $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;
}

以上示例没有考虑到如何在iframe中显示3D Secure表单而不是内联。这超出了这个简单描述的范围。使用iframe时需要考虑两个主要问题:1) 上面的表单必须通过名称target iframe;2) 返回到$termUrl时,页面必须从iframe中跳出。这是绝对的基本要求。

然后这个表单将引导用户到3D Secure密码页面。对于Sage Pay测试,使用代码password在达到测试3D Secure表单时获取成功的响应。

现在您需要处理银行的返回。使用Guzzle,您可以将返回的消息捕获为一个PSR-7 ServerRequest,如下所示

use Academe\Opayo\Pi\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);
    ...
}

use Academe\Opayo\Pi\ServerRequest\Secure3DAcs;

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

两者都可以正常工作,但这主要取决于您的框架和应用程序的最佳选择。

处理3D Secure结果涉及两个步骤

  1. 将结果传递给Sage Pay以获取3D Secure状态(注意:见下文)。
  2. 从Sage Pay获取最终的交易结果。
    use Academe\Opayo\Pi\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);
    $secure3dResponse = ResponseFactory::fromHttpResponse($response);

    // This will be the result. We are looking for `Authenticated` or similar.
    // The $secure3dResponse will normally be the full transaction details.
    //
    // 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();

3D Secure 后的最终交易

无论3D Secure是否通过,都要获取交易。然而 - 不要过早获取。Sage Pay的测试实例在获取3D Secure结果和能够获取交易之间存在轻微的延迟。在这种情况下,最好暂停一秒钟,这是一个任意的周期,但现在似乎有效。更好的方法是立即尝试,如果收到404错误,就短暂退后一段时间再次尝试,如果需要,可能再试一次。这应该在网关中多次修复,但偶尔仍然报告为问题。

    // 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);

    // 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);

3D Secure 版本 2

这将在2022年在全球范围内成为强制性的,所以现在开始使用它。

SCA:强客户身份验证

3D Secure版本2流程由提供SCA信息触发。以下示例是一个最小的SCA对象。Opayo网站上没有关于如何收集所有这些信息以及如何使用它们的指南,并且默认值很少,因此需要进行一些实验。

请注意,与3DS v1不同,通知URL - 用户返回结果的地方 - 在一开始就提供,无论是否需要重定向。

// When using 3D Secure v2, put together additional SCA details.

$strongCustomerAuthentication = new StrongCustomerAuthentication(
    'https://example.com/your-3dsecure-notification-handler-post-url/',
    $_SERVER['REMOTE_ADDR'], // IPv4 of user's browser
    $_SERVER['HTTP_ACCEPT'], // Full Accept header provided by user's browser
    true, // if javascript enabled on the browser; your payment page would need to detect that
    'en-GB', // Language of the user's browser; docs are ambiguous on whether "en-GB" or just "en"
    $_SERVER['HTTP_USER_AGENT'], // Full user agent of the user's browser
    StrongCustomerAuthentication::CHALLENGE_WINDOW_SIZE_FULLSCREEN,
    StrongCustomerAuthentication::TRANS_TYPE_GOODS_AND_SERVICE_PURCHASE,
    [
        // These are mandatory if javascript is enabled.
        'browserJavaEnabled' => false,
        'browserColorDepth' => StrongCustomerAuthentication::BROWSER_COLOR_DEPTH_32,
        'browserScreenHeight' => 512,
        'browserScreenWidth' => 1024,
        'browserTz' => 60,
    ]
);

当创建付款时,这将作为创建的附加选项传递。

$paymentRequest = new CreatePayment(
    ...
    [
        // 3D Secure v2 needs Strong Customer Authentication (SCA) which requires
        // additional browser details.
        'strongCustomerAuthentication' => $strongCustomerAuthentication,
    ]
);

3D Secure 版本 2 重定向

银行或3DS规则可能决定不需要进一步的验证,因此不需要重定向,然后您将立即收到交易详情。

如果需要重定向,则通过类似于3DS v1的POST完成。

// Example 3DS POST redirect with a button.

// The $threeDSSessionData is an optional string that can be passed to the ACS,
// and will be returned with the result to help match up the user with their
// payment request.

// Note: do NOT pass the transactionId in as the threeDSSessionData. If you do
// this, Opayo will reject the 3DS redirect challenge. It's not known why, but
// has been observed. The vendorTxCode can be used with no issues at this time.

if ($transactionResponse->isRedirect()) {
    $encThreeDSSessionData = base64_encode($threeDSSessionData);

    echo '<form method="post" action="'.$payment->getAcsUrl().'">';
    foreach($transactionResponse->getPaRequestFields($encThreeDSSessionData) as $name => $value) {
        echo '<input type="hidden" name="'.$name.'" value="'.$value.'" />';
    }
    echo '<button type="submit">Click here if not redirected in five seconds</button>';
    echo '</form>';
}

3D Secure 版本 2 通知处理

重定向后,ACS将用户返回到您的通知URL并带有一个结果。检查响应是否来自ACS并实例化服务器请求对象

use Academe\Opayo\Pi\ServerRequest\Secure3Dv2Notification;

if (Secure3Dv2Notification::isRequest($_POST)) {
    $secure3Dv2Notification = new Secure3Dv2Notification::fromData($_POST);
    ...
    // If you need the sent session data, it can be found here:

    $encThreeDSSessionData = $secure3Dv2Notification->getThreeDSSessionData();
    $threeDSSessionData = base64_decode($encThreeDSSessionData);

}

最后,使用该结果获取交易授权结果。$transactionId应该是原始CreatePayment请求返回的值。

    use Academe\Opayo\Pi\Request\CreateSecure3Dv2Challenge;

    $request = new CreateSecure3Dv2Challenge(
        $endpoint, $auth, $notification, $transactionId
    );

    $response = $client->sendRequest($request);
    $transaction = ResponseFactory::fromHttpResponse($response);

保存和重复使用卡

有关可重复使用的令牌,请参阅Opayo文档.

以前的交易可以作为重复付款的基础。这必须在您的初始请求中设置正确

...

if ($saveCard) {
    $card = $card->withSave();
}

$paymentRequest = new CreatePayment(
    $endpoint,
    $auth,
    $card,
    'MyVendorTxCode-' . rand(10000000, 99999999),
    $amount,
    'My Purchase Description',
    $billingAddress,
    $customer
);

if ($saveCard) {
    // A credential type must be applied. There are convienence static creators to simplify this
    // @see https://developer-eu.elavon.com/docs/opayo/credential-file-0
     
    $paymentRequest->setCredentialType(CredentialType::createForNewReusableCard());

    // Saving a token for reuse is only possible when 3D secure is applied (the cardholder must be present)

    $paymentRequest->withApply3DSecure(CreatePayment::APPLY_3D_SECURE_FORCE);
}

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

在3D Secure响应之后,您可以保存返回的卡详情

    $request = new CreateSecure3Dv2Challenge(...);
    $response = $client->sendRequest($request);
    $response = ResponseFactory::fromHttpResponse($response);

    $card = $response->getPaymentMethod();

    if ($card instanceof Card && $card->isReusable()) {
        // store this info against your customer for future use
        $serialisedCard = json_encode($card);
    
        // or individual attributes of the card if you want it normalised
        $savedToken = $card->getCardIdentifier();
        $lastFourDigits = $card->getLastFourDigits();
        $expiryDate = $card->getExpiryDate();
    }   

在未来的请求中,您可以使用该令牌。您可以修改运货详情和金额(没有限制),但不能修改收款人详情或地址。

商户/管理员用户可以使用客户的储蓄卡代表客户下单。他们可以完全绕过CVC的需求,但需要在Opayo上启用电话/MOTO功能(在沙盒中不可用)。

您还需要在请求中提供凭证类型。

if ($merchantPlacingOrderOnCustomersBehalf) {
    $card = new ReusableCard($savedToken);
} else {
    // customer placing order themselves, requires CVV submitted through JS
    $card = new ReusableCvvCard($sessionKey, $cardIdentifier);
}

$paymentRequest = new CreatePayment(
    $endpoint,
    $auth,
    $card,
    'MyVendorTxCode-' . rand(10000000, 99999999),
    $amount,
    'My Purchase Description',
    $billingAddress,
    $customer
);

if ($merchantPlacingOrderOnCustomersBehalf) {
    $paymentRequest->setEntryMethod(CreatePayment::ENTRY_METHOD_TELEPHONEORDER);
    $paymentRequest->withApplyAvsCvcCheck(CreatePayment::APPLY_AVS_CVC_CHECK_DISABLE);
    $paymentRequest->setCredentialType(CredentialType::createForMerchantReusingCard());
} else {
    $paymentRequest->setCredentialType(CredentialType::createForCustomerReusingCard());
}

支付方式

目前,Sage Pay Pi仅支持card支付类型。然而,有三种不同的卡对象类型

  1. SingleUseCard - 卡第一次使用时。它已被令牌化,并将保留在商户会话密钥中400秒,然后被丢弃。
  2. ReusableCard - 已保存并可重复使用的卡片。当不使用CVV时,用于非交互式支付。
  3. ReusableCvvCard - 已保存并可重复使用的卡片,且已与CVV和商户会话关联。用于需要用户输入CVV以增加安全性的卡片交互式重复使用,但无需重新输入所有卡信息。CVV通常在客户端与卡片和商户会话关联,因此将在有限时间内(400秒)保持活跃。

ReusableCard 不需要商户会话密钥。ReusableCvvCard 需要商户会话密钥以及一个将会话密钥、卡片标识符和CVV关联起来的调用(最好在客户端进行,但在适当PCI认证或测试期间也可以在服务器端进行)。

可以通过LinkSecurityCode消息将CVV与可重复使用的卡片关联。

use Academe\Opayo\Pi\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)
);

// Should check for errors here:

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

要保存可重复使用的卡片,从成功的支付中获取PaymentMethod。注意:目前无法在不进行支付的情况下设置可重复使用的卡片。这是网关的限制。某些网关允许您创建零金额支付以进行验证和设置可重复使用的卡片,但这里不行。

...

// 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);