Skip to content

Commit eb72a41

Browse files
committed
Update
* add refund() support, including pending state * add fetchTransaction() support * add support for MOTO transactions * update complete purchase requests to validate necessary URL parameters * update requests to use a common local AbstractRequest
1 parent 9c2b804 commit eb72a41

17 files changed

+579
-120
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
},
4444
"extra": {
4545
"branch-alias": {
46-
"dev-main": "3.0.x-dev"
46+
"dev-main": "3.1.x-dev"
4747
}
4848
},
4949
"prefer-stable": true,

src/Gateway.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use Omnipay\Common\AbstractGateway;
66
use Omnipay\Worldline\Message\CompletePurchaseRequest;
7+
use Omnipay\Worldline\Message\FetchTransactionRequest;
78
use Omnipay\Worldline\Message\PurchaseRequest;
9+
use Omnipay\Worldline\Message\RefundRequest;
810

911
/**
1012
* Worldline Hosted Checkout Gateway
@@ -69,4 +71,14 @@ public function completePurchase(array $parameters = [])
6971
{
7072
return $this->createRequest(CompletePurchaseRequest::class, $parameters);
7173
}
74+
75+
public function refund(array $parameters = [])
76+
{
77+
return $this->createRequest(RefundRequest::class, $parameters);
78+
}
79+
80+
public function fetchTransaction(array $parameters = [])
81+
{
82+
return $this->createRequest(FetchTransactionRequest::class, $parameters);
83+
}
7284
}

src/Message/AbstractRequest.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Omnipay\Worldline\Message;
4+
5+
use DateTime;
6+
use DateTimeZone;
7+
use Money\Currency;
8+
use Money\Number;
9+
use Money\Parser\DecimalMoneyParser;
10+
use Omnipay\Common\Message\AbstractRequest as BaseAbstractRequest;
11+
use Omnipay\Common\Exception\InvalidRequestException;
12+
13+
/**
14+
* Base request for Worldline
15+
*
16+
* @see https://docs.direct.worldline-solutions.com/en/integration/api-developer-guide/authentication
17+
*/
18+
abstract class AbstractRequest extends BaseAbstractRequest
19+
{
20+
/** @var string */
21+
protected $liveEndpoint = 'https://payment.direct.worldline-solutions.com';
22+
/** @var string */
23+
protected $testEndpoint = 'https://payment.preprod.direct.worldline-solutions.com';
24+
/** @var string HTTP verb used to make the request */
25+
protected $requestMethod = 'GET';
26+
27+
public function getApiKey()
28+
{
29+
return $this->getParameter('apiKey');
30+
}
31+
32+
public function setApiKey($value)
33+
{
34+
return $this->setParameter('apiKey', $value);
35+
}
36+
37+
public function getApiSecret()
38+
{
39+
return $this->getParameter('apiSecret');
40+
}
41+
42+
public function setApiSecret($value)
43+
{
44+
return $this->setParameter('apiSecret', $value);
45+
}
46+
47+
public function getMerchantId()
48+
{
49+
return $this->getParameter('merchantId');
50+
}
51+
52+
public function setMerchantId($value)
53+
{
54+
return $this->setParameter('merchantId', $value);
55+
}
56+
57+
public function getEndpoint()
58+
{
59+
return ($this->getTestMode() ? $this->testEndpoint : $this->liveEndpoint).$this->getAction();
60+
}
61+
62+
public function sendData($data)
63+
{
64+
$contentType = $this->requestMethod == 'POST' ? 'application/json; charset=utf-8' : '';
65+
$now = new DateTime('now', new DateTimeZone('GMT'));
66+
$dateTime = $now->format("D, d M Y H:i:s T");
67+
$endpointAction = $this->getAction();
68+
69+
$message = $this->requestMethod."\n".$contentType."\n".$dateTime."\n".$endpointAction."\n";
70+
$signature = $this->createSignature($message, $this->getApiSecret());
71+
72+
$headers = [
73+
'Content-Type' => $contentType,
74+
'Authorization' => 'GCS v1HMAC:'.$this->getApiKey().':'.$signature,
75+
'Date' => $dateTime,
76+
];
77+
78+
$body = json_encode($data);
79+
80+
$httpResponse = $this->httpClient->request(
81+
$this->requestMethod,
82+
$this->getEndpoint(),
83+
$headers,
84+
$body
85+
);
86+
87+
return $this->createResponse($httpResponse->getBody()->getContents());
88+
}
89+
90+
/**
91+
* @param mixed $data
92+
* @return AbstractResponse
93+
*/
94+
abstract protected function createResponse($data);
95+
96+
/**
97+
* @return string
98+
*/
99+
abstract protected function getAction();
100+
101+
/**
102+
* Create signature hash used to verify messages
103+
*
104+
* @param string $message The message to encrypt
105+
* @param string $key The base64-encoded key used to encrypt the message
106+
*
107+
* @return string Generated signature
108+
*/
109+
protected function createSignature($message, $key)
110+
{
111+
return base64_encode(hash_hmac('sha256', $message, $key, true));
112+
}
113+
114+
/**
115+
* Get integer version (sallest unit) of item price
116+
*
117+
* Copied from {@see BaseAbstractRequest::getAmountInteger()} & {@see BaseAbstractRequest::getMoney()}
118+
*/
119+
protected function getItemPriceInteger($item)
120+
{
121+
$currencyCode = $this->getCurrency() ?: 'USD';
122+
$currency = new Currency($currencyCode);
123+
$amount = $item->getPrice();
124+
125+
$moneyParser = new DecimalMoneyParser($this->getCurrencies());
126+
$number = Number::fromString($amount);
127+
// Check for rounding that may occur if too many significant decimal digits are supplied.
128+
$decimal_count = strlen($number->getFractionalPart());
129+
$subunit = $this->getCurrencies()->subunitFor($currency);
130+
if ($decimal_count > $subunit) {
131+
throw new InvalidRequestException('Amount precision is too high for currency.');
132+
}
133+
$money = $moneyParser->parse((string) $number, $currency);
134+
135+
return (int) $money->getAmount();
136+
}
137+
}

src/Message/CompletePurchaseRequest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
*
88
* @see https://docs.direct.worldline-solutions.com/en/api-reference#tag/HostedCheckout/operation/GetHostedCheckoutApi
99
*/
10-
class CompletePurchaseRequest extends PurchaseRequest
10+
class CompletePurchaseRequest extends AbstractRequest
1111
{
12-
protected $requestMethod = 'GET';
13-
1412
public function getHostedCheckoutId()
1513
{
1614
return $this->getParameter('hostedCheckoutId');
@@ -23,6 +21,8 @@ public function setHostedCheckoutId($value)
2321

2422
public function getData()
2523
{
24+
$this->validate('merchantId', 'hostedCheckoutId');
25+
2626
return $this->httpRequest->request->all();
2727
}
2828

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Omnipay\Worldline\Message;
4+
5+
/**
6+
* Worldline Fetch Transaction Request
7+
*
8+
* @see https://docs.direct.worldline-solutions.com/en/api-reference#tag/Payments/operation/GetPaymentDetailsApi
9+
*/
10+
class FetchTransactionRequest extends AbstractRequest
11+
{
12+
public function getData()
13+
{
14+
$this->validate('merchantId', 'transactionReference');
15+
return [];
16+
}
17+
18+
protected function createResponse($data)
19+
{
20+
return $this->response = new FetchTransactionResponse($this, json_decode($data));
21+
}
22+
23+
protected function getAction()
24+
{
25+
return '/v2/'.$this->getMerchantId().'/payments/'.$this->getTransactionReference().'/details';
26+
}
27+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Omnipay\Worldline\Message;
4+
5+
use Omnipay\Common\Message\AbstractResponse;
6+
7+
/**
8+
* Worldline Fetch Transaction Response
9+
*/
10+
class FetchTransactionResponse extends AbstractResponse
11+
{
12+
/**
13+
* Is the response pending success or failure?
14+
*
15+
* @return boolean
16+
*/
17+
public function isPending()
18+
{
19+
if (empty($this->data) || isset($this->data->errorId)) {
20+
return false;
21+
}
22+
23+
return $this->data->statusOutput->statusCategory == 'PENDING_CONNECT_OR_3RD_PARTY';
24+
}
25+
26+
/**
27+
* Is the response successful?
28+
*
29+
* @return boolean
30+
*/
31+
public function isSuccessful()
32+
{
33+
if (empty($this->data) || isset($this->data->errorId)) {
34+
return false;
35+
}
36+
37+
return in_array($this->data->statusOutput->statusCategory, ['COMPLETED', 'REFUNDED']);
38+
}
39+
40+
/**
41+
* Numeric status code (also in back office / report files)
42+
*
43+
* @return null|string
44+
*/
45+
public function getCode()
46+
{
47+
return $this->data->statusOutput->statusCode ?? $this->data->errors[0]->errorCode ?? null;
48+
}
49+
50+
/**
51+
* Get the authorisation code if available.
52+
*
53+
* @return null|string
54+
*/
55+
public function getTransactionReference()
56+
{
57+
return $this->data->id ?? null;
58+
}
59+
60+
/**
61+
* Get the merchant response message if available.
62+
*
63+
* @return null|string
64+
*/
65+
public function getMessage()
66+
{
67+
return $this->data->status ?? $this->data->errorId ?? null;
68+
}
69+
}

0 commit comments

Comments
 (0)