Skip to content

Commit bfa576e

Browse files
committed
Feature - Opayo card tokens (#1602)
1 parent 2872c98 commit bfa576e

10 files changed

+437
-69
lines changed

README.md

+168-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,169 @@
1-
# Lunar Opayo Payments
1+
<p align="center"><img src="https://github.com/lunarphp/lunar/assets/1488016/a21f1cfb-9259-4d21-9bb0-eca876957729" width="300" ></p>
22

3-
WIP
3+
4+
5+
<p align="center">This addon enables Opayo payments on your Lunar storefront.</p>
6+
7+
## Alpha Release
8+
9+
This addon is currently in Alpha, whilst every step is taken to ensure this is working as intended, it will not be considered out of Alpha until more tests have been added and proved.
10+
11+
## Minimum Requirements
12+
13+
- Lunar `1.x`
14+
- An [Elavon](https://www.elavon.com/) merchant account
15+
16+
## Installation
17+
18+
### Require the composer package
19+
20+
```sh
21+
composer require lunarphp/opayo
22+
```
23+
24+
### Configure the service
25+
26+
Add the opayo config to the `config/services.php` file.
27+
28+
```php
29+
// ...
30+
'opayo' => [
31+
'vendor' => env('OPAYO_VENDOR'),
32+
'env' => env('OPAYO_ENV', 'test'),
33+
'key' => env('OPAYO_KEY'),
34+
'password' => env('OPAYO_PASSWORD'),
35+
'host' => env('OPAYO_HOST'),
36+
],
37+
```
38+
39+
40+
### Enable the driver
41+
42+
Set the driver in `config/lunar/payments.php`
43+
44+
```php
45+
<?php
46+
47+
return [
48+
// ...
49+
'types' => [
50+
'card' => [
51+
// ...
52+
'driver' => 'opayo',
53+
],
54+
],
55+
];
56+
```
57+
58+
59+
60+
## Configuration
61+
62+
Below is a list of the available configuration options this package uses in `config/lunar/opayo.php`
63+
64+
| Key | Default | Description |
65+
| --- | --- | --- |
66+
| `policy` | `automatic` | Determines the policy for taking payments and whether you wish to capture the payment manually later or take payment straight away. Available options `deferred` or `automatic` |
67+
68+
---
69+
70+
## Backend Usage
71+
72+
### Get a merchant key
73+
74+
```php
75+
Lunar\Opayo\Facades\Opayo::getMerchantKey();
76+
```
77+
78+
### Authorize a charge
79+
80+
```php
81+
$response = \Lunar\Facades\Payments::driver('opayo')->cart(
82+
$cart = CartSession::current()->calculate()
83+
)->withData([
84+
'merchant_key' => $request->get('merchantSessionKey'),
85+
'card_identifier' => $request->get('cardToken'),
86+
'browserLanguage' => $request->get('browserLanguage'),
87+
'challengeWindowSize' => $request->get('challengeWindowSize'),
88+
'browserIP' => $request->ip(),
89+
'browserAcceptHeader' => $request->header('accept'),
90+
'browserUserAgent' => $request->get('browserUserAgent'),
91+
'browserJavaEnabled' => $request->get('browserJavaEnabled', false),
92+
'browserColorDepth' => $request->get('browserColorDepth'),
93+
'browserScreenHeight' => $request->get('browserScreenHeight'),
94+
'browserScreenWidth' => $request->get('browserScreenWidth'),
95+
'browserTZ' => $request->get('browserTZ'),
96+
'status' => 'payment-received',
97+
])->authorize();
98+
```
99+
100+
When authorizing a charge, you may be required to submit extra authentication in the form of 3DSV2, you can handle this in your payment endpoint.
101+
102+
```php
103+
if (is_a($response, \Lunar\Opayo\Responses\ThreeDSecureResponse::class)) {
104+
return response()->json([
105+
'requires_auth' => true,
106+
'data' => $response,
107+
]);
108+
}
109+
```
110+
111+
`$response` will contain all the 3DSV2 information from Opayo.
112+
113+
You can find more information about this using the following links:
114+
115+
- [3-D Secure explained](https://www.elavon.co.uk/resource-center/help-with-your-solutions/opayo/fraud-prevention/3D-Secure.html)
116+
- [3D Secure Transactions](https://developer.elavon.com/products/opayo-direct/v1/3d-secure-transactions)
117+
- Stack overflow [SagePay 3D Secure V2 Flow](https://stackoverflow.com/questions/65329436/sagepay-3d-secure-v2-flow)
118+
119+
Once you have handled the 3DSV2 response on your storefront, you can then authorize again.
120+
121+
```php
122+
$response = Payments::driver('opayo')->cart(
123+
$cart = CartSession::current()->calculate()
124+
)->withData([
125+
'cres' => $request->get('cres'),
126+
'pares' => $request->get('pares'),
127+
'transaction_id' => $request->get('transaction_id'),
128+
])->threedsecure();
129+
130+
if (! $response->success) {
131+
abort(401);
132+
}
133+
134+
```
135+
136+
### Opayo card tokens
137+
138+
When authenticated users make an order on your store, it can be good to offer the ability to save their card information for future use. Whilst we don't store the actual card details, we can use card tokens which represent the card the user has used before.
139+
140+
> You must have saved payments enabled on your Opayo account because you can use these.
141+
142+
To save a card, pass in the `saveCard` data key when authorizing a payment.
143+
144+
```php
145+
$response = \Lunar\Facades\Payments::driver('opayo')->cart(
146+
$cart = CartSession::current()->calculate()
147+
)->withData([
148+
// ...
149+
'saveCard' => true
150+
])->authorize();
151+
```
152+
153+
Assuming everything went well, there will be a new entry in the `opayo_tokens` table, associated to the authenticated user. You can then display these card representations at checkout for the user to select. The `token` is what replaces the `card_identifier` data key.
154+
155+
```php
156+
$response = \Lunar\Facades\Payments::driver('opayo')->cart(
157+
$cart = CartSession::current()->calculate()
158+
)->withData([
159+
// ...
160+
'card_identifier' => $request->get('cardToken'),
161+
'reusable' => true
162+
])->authorize();
163+
```
164+
165+
Responses are then handled the same as any other transaction.
166+
167+
## Contributing
168+
169+
Contributions are welcome, if you are thinking of adding a feature, please submit an issue first so we can determine whether it should be included.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
-<?php
2+
3+
use Illuminate\Database\Schema\Blueprint;
4+
use Illuminate\Support\Facades\Schema;
5+
use Lunar\Base\Migration;
6+
7+
class CreateOpayoTokensTable extends Migration
8+
{
9+
public function up()
10+
{
11+
Schema::create($this->prefix.'opayo_tokens', function (Blueprint $table) {
12+
$table->userForeignKey();
13+
$table->string('card_type')->index();
14+
$table->string('last_four');
15+
$table->string('token');
16+
$table->string('auth_code')->nullable();
17+
$table->timestamp('expires_at');
18+
$table->timestamps();
19+
});
20+
}
21+
22+
public function down()
23+
{
24+
Schema::dropIfExists($this->prefix.'opayo_tokens');
25+
}
26+
}

src/Components/PaymentForm.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public function process()
140140
], $this->browser))->authorize();
141141

142142
if ($result->success) {
143-
if ($result->status == Opayo::THREE_D_AUTH) {
143+
if ($result->status == Opayo::THREED_AUTH) {
144144
$this->threeDSecure['acsUrl'] = $result->acsUrl;
145145
$this->threeDSecure['acsTransId'] = $result->acsTransId;
146146
$this->threeDSecure['dsTransId'] = $result->dsTransId;

src/Concerns/HasOpayoTokens.php

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Lunar\Opayo\Concerns;
4+
5+
use Illuminate\Database\Eloquent\Relations\HasMany;
6+
use Lunar\Opayo\Models\OpayoToken;
7+
8+
trait HasOpayoTokens
9+
{
10+
public function opayoTokens(): HasMany
11+
{
12+
return $this->hasMany(OpayoToken::class);
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Lunar\Opayo\DataTransferObjects;
4+
5+
class AuthPayloadParameters
6+
{
7+
public function __construct(
8+
public string $transactionType,
9+
public string $merchantSessionKey,
10+
public string $cardIdentifier,
11+
public string $vendorTxCode,
12+
public int $amount,
13+
public string $currency,
14+
public string $customerFirstName,
15+
public string $customerLastName,
16+
public string $billingAddressLineOne,
17+
public string $billingAddressCity,
18+
public string $billingAddressPostcode,
19+
public string $billingAddressCountryIso,
20+
public ?string $customerMobilePhone,
21+
public string $notificationURL,
22+
public ?string $browserLanguage,
23+
public ?string $challengeWindowSize,
24+
public ?string $browserIP,
25+
public ?string $browserAcceptHeader,
26+
public bool $browserJavascriptEnabled,
27+
public ?string $browserUserAgent,
28+
public bool $browserJavaEnabled,
29+
public ?string $browserColorDepth,
30+
public ?string $browserScreenHeight,
31+
public ?string $browserScreenWidth,
32+
public ?string $browserTZ,
33+
public bool $saveCard = false,
34+
public bool $reusable = false,
35+
public ?string $authCode = null
36+
) {
37+
}
38+
}

src/Facades/Opayo.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
namespace Lunar\Opayo\Facades;
44

55
use Illuminate\Support\Facades\Facade;
6+
use Lunar\Opayo\DataTransferObjects\AuthPayloadParameters;
67
use Lunar\Opayo\OpayoInterface;
78

9+
/**
10+
* @method static getAuthPayload(AuthPayloadParameters $parameters): array
11+
*/
812
class Opayo extends Facade
913
{
1014
/**
@@ -20,7 +24,7 @@ class Opayo extends Facade
2024
/**
2125
* Status when the payment requires Three D Secure authentication.
2226
*/
23-
const THREE_D_AUTH = 20;
27+
const THREED_AUTH = 20;
2428

2529
/**
2630
* Status for when Three D Secure fails.

src/Models/OpayoToken.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Lunar\Opayo\Models;
4+
5+
use Lunar\Base\BaseModel;
6+
7+
/**
8+
* @property int $id
9+
* @property int $user_id;
10+
* @property string $card_type
11+
* @property string $last_four
12+
* @property string $token
13+
* @property ?string $auth_code
14+
* @property \Illuminate\Support\Carbon $expires_at
15+
* @property ?\Illuminate\Support\Carbon $created_at
16+
* @property ?\Illuminate\Support\Carbon $updated_at
17+
*/
18+
class OpayoToken extends BaseModel
19+
{
20+
/**
21+
* Define which attributes should be
22+
* protected from mass assignment.
23+
*
24+
* @var array
25+
*/
26+
protected $guarded = [];
27+
28+
protected $casts = [
29+
'expires_at' => 'datetime',
30+
];
31+
}

src/Opayo.php

+68
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Lunar\Opayo;
44

55
use Illuminate\Support\Facades\Http;
6+
use Lunar\Opayo\DataTransferObjects\AuthPayloadParameters;
67

78
class Opayo implements OpayoInterface
89
{
@@ -75,6 +76,73 @@ public function getTransaction($id, $attempt = 1)
7576
return $response->object();
7677
}
7778

79+
public function getAuthPayload(AuthPayloadParameters $parameters): array
80+
{
81+
$payload = [
82+
'transactionType' => $parameters->transactionType,
83+
'paymentMethod' => [
84+
'card' => [
85+
'merchantSessionKey' => $parameters->merchantSessionKey,
86+
'cardIdentifier' => $parameters->cardIdentifier,
87+
],
88+
],
89+
'vendorTxCode' => $parameters->vendorTxCode,
90+
'amount' => $parameters->amount,
91+
'currency' => $parameters->currency,
92+
'description' => 'Webstore Transaction',
93+
'apply3DSecure' => 'UseMSPSetting',
94+
'customerFirstName' => $parameters->customerFirstName,
95+
'customerLastName' => $parameters->customerLastName,
96+
'billingAddress' => [
97+
'address1' => $parameters->billingAddressLineOne,
98+
'city' => $parameters->billingAddressCity,
99+
'postalCode' => $parameters->billingAddressPostcode,
100+
'country' => $parameters->billingAddressCountryIso,
101+
],
102+
'strongCustomerAuthentication' => [
103+
'customerMobilePhone' => $parameters->customerMobilePhone,
104+
'transType' => 'GoodsAndServicePurchase',
105+
'browserLanguage' => $parameters->browserLanguage,
106+
'challengeWindowSize' => $parameters->challengeWindowSize,
107+
'browserIP' => $parameters->browserIP,
108+
'notificationURL' => $parameters->notificationURL,
109+
'browserAcceptHeader' => $parameters->browserAcceptHeader,
110+
'browserJavascriptEnabled' => true,
111+
'browserUserAgent' => $parameters->browserUserAgent,
112+
'browserJavaEnabled' => $parameters->browserJavaEnabled,
113+
'browserColorDepth' => $parameters->browserColorDepth,
114+
'browserScreenHeight' => $parameters->browserScreenHeight,
115+
'browserScreenWidth' => $parameters->browserScreenWidth,
116+
'browserTZ' => $parameters->browserTZ,
117+
],
118+
'entryMethod' => 'Ecommerce',
119+
];
120+
121+
if ($parameters->saveCard) {
122+
$payload['credentialType'] = [
123+
'cofUsage' => 'First',
124+
'initiatedType' => 'CIT',
125+
'mitType' => 'Unscheduled',
126+
];
127+
$payload['paymentMethod']['card']['save'] = true;
128+
}
129+
130+
if ($parameters->reusable) {
131+
$payload['credentialType'] = [
132+
'cofUsage' => 'Subsequent',
133+
'initiatedType' => 'CIT',
134+
'mitType' => 'Unscheduled',
135+
];
136+
$payload['paymentMethod']['card']['reusable'] = true;
137+
}
138+
139+
if ($parameters->authCode) {
140+
$payload['strongCustomerAuthentication']['threeDSRequestorPriorAuthenticationInfo']['threeDSReqPriorRef'] = $parameters->authCode;
141+
}
142+
143+
return $payload;
144+
}
145+
78146
/**
79147
* Get the service credentials.
80148
*

0 commit comments

Comments
 (0)