i3 / opayo-pi
用于处理Opayo Pi(曾是Sage Pay集成)支付网关的消息内容和结构的库
Requires
- php: ^7.3|^8.0
- ext-json: *
- alcohol/iso4217: ^3.0|^4.0
- psr/http-message: ^1.0
- shrikeh/teapot: ^1.0|^2.0
Requires (Dev)
- phpunit/phpunit: ^4.8.0
Suggests
- guzzlehttp/guzzle: ^6.0|^7.0|^8.0
- guzzlehttp/psr7: ^1.2.0
- moneyphp/money: ^3.0
This package is auto-updated.
Last update: 2024-09-12 09:17:07 UTC
README
注意
由于composer包名的更改,此包是从academe/SagePay-Integration分支复制到新仓库的。
Opayo Pi PSR-7 消息 REST API 库
此包为Opayo Pi(曾是Sage Pay集成)支付网关提供数据模型。它不提供传输机制,因此您可以使用您喜欢的PSR-18客户端,例如Guzzle(7+或6+HTTPlug适配器)、curl或其他PSR-7库。
您可以使用此库作为PSR-7消息生成/消费者,或者深入一层,通过数组处理所有数据 - 这两种方式都受到支持。
此库相当古老,所以有些地方有点笨拙,但随着时间的推移应该会改进。
包开发
Sage Pay集成支付网关是由Sage Pay运行的一个RESTful API。您可以通过这里申请账户(我的合作伙伴链接)。
从v3.0.0版开始,此包正在重新命名为Opayo,获得新的composer名称和新的基本命名空间。
PSR7
分支现在仅处于维护模式,不会进行任何重大更改 - 只会修复报告的bug。目标是当demo(和一些单元测试)准备就绪后,在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的表单集成JavaScript或Drop-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 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: $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
对象。此消息将返回true for isRedirect()
。鉴于这一点,需要一个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; }
上述示例没有考虑如何将3D Secure表单显示在iframe中而不是内联。这不属于本简单描述的范围。使用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结果涉及两个步骤:
- 将结果传递给Sage Pay以获取3D Secure状态(注意:见下文)。
- 从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(用户返回结果的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. if ($transactionResponse->isRedirect()) { echo '<form method="post" action="'.$payment->getAcsUrl().'">'; foreach($transactionResponse->getPaRequestFields($threeDSSessionData) 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: $threeDSSessionData = $secure3Dv2Notification->getThreeDSSessionData(); }
最后使用该结果来获取交易授权结果。
use Academe\Opayo\Pi\Request\CreateSecure3Dv2Challenge; $request = new CreateSecure3Dv2Challenge( $endpoint, $auth, $notification, $transactionId ); $response = $client->sendRequest($request); $transaction = ResponseFactory::fromHttpResponse($response);
支付方式
目前,Sage Pay Pi仅支持card
支付类型。然而,有三种不同类型的卡片对象:
SingleUseCard
- 第一次使用卡片。它已被标记化,并将保留在商户会话密钥中400秒后丢弃。ReusableCard
- 已保存并可重复使用的卡片。在未使用CVV的非交互式支付中使用此卡。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);