1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @license LGPLv3, http://opensource.org/licenses/LGPL-3.0 |
5
|
|
|
* @copyright Metaways Infosystems GmbH, 2012 |
6
|
|
|
* @copyright Aimeos (aimeos.org), 2015-2017 |
7
|
|
|
* @package MShop |
8
|
|
|
* @subpackage Service |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
|
12
|
|
|
namespace Aimeos\MShop\Service\Provider\Payment; |
13
|
|
|
|
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Payment provider for paypal express orders. |
17
|
|
|
* |
18
|
|
|
* @package MShop |
19
|
|
|
* @subpackage Service |
20
|
|
|
*/ |
21
|
|
|
class PayPalExpress |
22
|
|
|
extends \Aimeos\MShop\Service\Provider\Payment\Base |
|
|
|
|
23
|
|
|
implements \Aimeos\MShop\Service\Provider\Payment\Iface |
24
|
|
|
{ |
25
|
|
|
private $apiendpoint; |
26
|
|
|
|
27
|
|
|
private $beConfig = array( |
28
|
|
|
'paypalexpress.ApiUsername' => array( |
29
|
|
|
'code' => 'paypalexpress.ApiUsername', |
30
|
|
|
'internalcode' => 'paypalexpress.ApiUsername', |
31
|
|
|
'label' => 'Username', |
32
|
|
|
'type' => 'string', |
33
|
|
|
'internaltype' => 'string', |
34
|
|
|
'default' => '', |
35
|
|
|
'required' => true, |
36
|
|
|
), |
37
|
|
|
'paypalexpress.AccountEmail' => array( |
38
|
|
|
'code' => 'paypalexpress.AccountEmail', |
39
|
|
|
'internalcode' => 'paypalexpress.AccountEmail', |
40
|
|
|
'label' => 'Registered e-mail address of the shop owner in PayPal', |
41
|
|
|
'type' => 'string', |
42
|
|
|
'internaltype' => 'string', |
43
|
|
|
'default' => '', |
44
|
|
|
'required' => true, |
45
|
|
|
), |
46
|
|
|
'paypalexpress.ApiPassword' => array( |
47
|
|
|
'code' => 'paypalexpress.ApiPassword', |
48
|
|
|
'internalcode' => 'paypalexpress.ApiPassword', |
49
|
|
|
'label' => 'Password', |
50
|
|
|
'type' => 'string', |
51
|
|
|
'internaltype' => 'string', |
52
|
|
|
'default' => '', |
53
|
|
|
'required' => true, |
54
|
|
|
), |
55
|
|
|
'paypalexpress.ApiSignature' => array( |
56
|
|
|
'code' => 'paypalexpress.ApiSignature', |
57
|
|
|
'internalcode' => 'paypalexpress.ApiSignature', |
58
|
|
|
'label' => 'Signature', |
59
|
|
|
'type' => 'string', |
60
|
|
|
'internaltype' => 'string', |
61
|
|
|
'default' => '', |
62
|
|
|
'required' => true, |
63
|
|
|
), |
64
|
|
|
'paypalexpress.ApiEndpoint' => array( |
65
|
|
|
'code' => 'paypalexpress.ApiEndpoint', |
66
|
|
|
'internalcode' => 'paypalexpress.ApiEndpoint', |
67
|
|
|
'label' => 'APIEndpoint', |
68
|
|
|
'type' => 'string', |
69
|
|
|
'internaltype' => 'string', |
70
|
|
|
'default' => 'https://api-3t.paypal.com/nvp', |
71
|
|
|
'required' => false, |
72
|
|
|
), |
73
|
|
|
'paypalexpress.PaypalUrl' => array( |
74
|
|
|
'code' => 'paypalexpress.PaypalUrl', |
75
|
|
|
'internalcode' => 'paypalexpress.PaypalUrl', |
76
|
|
|
'label' => 'PaypalUrl', |
77
|
|
|
'type' => 'string', |
78
|
|
|
'internaltype' => 'string', |
79
|
|
|
'default' => 'https://www.paypal.com/webscr&cmd=_express-checkout&useraction=commit&token=%1$s', |
80
|
|
|
'required' => false, |
81
|
|
|
), |
82
|
|
|
'paypalexpress.PaymentAction' => array( |
83
|
|
|
'code' => 'paypalexpress.PaymentAction', |
84
|
|
|
'internalcode' => 'paypalexpress.PaymentAction', |
85
|
|
|
'label' => 'PaymentAction', |
86
|
|
|
'type' => 'string', |
87
|
|
|
'internaltype' => 'string', |
88
|
|
|
'default' => 'Sale', |
89
|
|
|
'required' => false, |
90
|
|
|
), |
91
|
|
|
'paypalexpress.LandingPage' => array( |
92
|
|
|
'code' => 'paypalexpress.LandingPage', |
93
|
|
|
'internalcode' => 'paypalexpress.LandingPage', |
94
|
|
|
'label' => 'Landing page', |
95
|
|
|
'type' => 'string', |
96
|
|
|
'internaltype' => 'string', |
97
|
|
|
'default' => 'Login', |
98
|
|
|
'required' => false, |
99
|
|
|
), |
100
|
|
|
'paypalexpress.FundingSource' => array( |
101
|
|
|
'code' => 'paypalexpress.FundingSource', |
102
|
|
|
'internalcode' => 'paypalexpress.FundingSource', |
103
|
|
|
'label' => 'Funding source', |
104
|
|
|
'type' => 'string', |
105
|
|
|
'internaltype' => 'string', |
106
|
|
|
'default' => 'CreditCard', |
107
|
|
|
'required' => false, |
108
|
|
|
), |
109
|
|
|
'paypalexpress.AddrOverride' => array( |
110
|
|
|
'code' => 'paypalexpress.AddrOverride', |
111
|
|
|
'internalcode' => 'paypalexpress.AddrOverride', |
112
|
|
|
'label' => 'Customer can change address', |
113
|
|
|
'type' => 'integer', |
114
|
|
|
'internaltype' => 'integer', |
115
|
|
|
'default' => 0, |
116
|
|
|
'required' => false, |
117
|
|
|
), |
118
|
|
|
'paypalexpress.NoShipping' => array( |
119
|
|
|
'code' => 'paypalexpress.NoShipping', |
120
|
|
|
'internalcode' => 'paypalexpress.NoShipping', |
121
|
|
|
'label' => 'Don\'t display shipping address', |
122
|
|
|
'type' => 'integer', |
123
|
|
|
'internaltype' => 'integer', |
124
|
|
|
'default' => 1, |
125
|
|
|
'required' => false, |
126
|
|
|
), |
127
|
|
|
'paypalexpress.url-validate' => array( |
128
|
|
|
'code' => 'paypalexpress.url-validate', |
129
|
|
|
'internalcode' => 'paypalexpress.url-validate', |
130
|
|
|
'label' => 'Validation URL', |
131
|
|
|
'type' => 'string', |
132
|
|
|
'internaltype' => 'string', |
133
|
|
|
'default' => 'https://www.paypal.com/webscr&cmd=_notify-validate', |
134
|
|
|
'required' => false, |
135
|
|
|
), |
136
|
|
|
); |
137
|
|
|
|
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Initializes the provider object. |
141
|
|
|
* |
142
|
|
|
* @param \Aimeos\MShop\Context\Item\Iface $context Context object |
143
|
|
|
* @param \Aimeos\MShop\Service\Item\Iface $serviceItem Service item with configuration |
144
|
|
|
* @throws \Aimeos\MShop\Service\Exception If one of the required configuration values isn't available |
145
|
|
|
*/ |
146
|
|
|
public function __construct( \Aimeos\MShop\Context\Item\Iface $context, \Aimeos\MShop\Service\Item\Iface $serviceItem ) |
147
|
|
|
{ |
148
|
|
|
parent::__construct( $context, $serviceItem ); |
149
|
|
|
|
150
|
|
|
$default = 'https://api-3t.paypal.com/nvp'; |
151
|
|
|
$this->apiendpoint = $this->getConfigValue( array( 'paypalexpress.ApiEndpoint' ), $default ); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* Returns the configuration attribute definitions of the provider to generate a list of available fields and |
157
|
|
|
* rules for the value of each field in the administration interface. |
158
|
|
|
* |
159
|
|
|
* @return array List of attribute definitions implementing \Aimeos\MW\Common\Critera\Attribute\Iface |
160
|
|
|
*/ |
161
|
|
|
public function getConfigBE() |
162
|
|
|
{ |
163
|
|
|
return $this->getConfigItems( $this->beConfig ); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* Checks the backend configuration attributes for validity. |
169
|
|
|
* |
170
|
|
|
* @param array $attributes Attributes added by the shop owner in the administraton interface |
171
|
|
|
* @return array An array with the attribute keys as key and an error message as values for all attributes that are |
172
|
|
|
* known by the provider but aren't valid |
173
|
|
|
*/ |
174
|
|
|
public function checkConfigBE( array $attributes ) |
175
|
|
|
{ |
176
|
|
|
$errors = parent::checkConfigBE( $attributes ); |
177
|
|
|
|
178
|
|
|
return array_merge( $errors, $this->checkConfig( $this->beConfig, $attributes ) ); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* Tries to get an authorization or captures the money immediately for the given order if capturing the money |
184
|
|
|
* separately isn't supported or not configured by the shop owner. |
185
|
|
|
* |
186
|
|
|
* @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object |
187
|
|
|
* @param array $params Request parameter if available |
188
|
|
|
* @return \Aimeos\MShop\Common\Item\Helper\Form\Standard Form object with URL, action and parameters to redirect to |
189
|
|
|
* (e.g. to an external server of the payment provider or to a local success page) |
190
|
|
|
*/ |
191
|
|
|
public function process( \Aimeos\MShop\Order\Item\Iface $order, array $params = [] ) |
192
|
|
|
{ |
193
|
|
|
$orderBaseItem = $this->getOrderBase( $order->getBaseId(), \Aimeos\MShop\Order\Item\Base\Base::PARTS_ALL ); |
194
|
|
|
|
195
|
|
|
$values = $this->getOrderDetails( $orderBaseItem ); |
196
|
|
|
$values['METHOD'] = 'SetExpressCheckout'; |
197
|
|
|
$values['PAYMENTREQUEST_0_INVNUM'] = $order->getId(); |
198
|
|
|
$values['RETURNURL'] = $this->getConfigValue( array( 'payment.url-success' ) ); |
199
|
|
|
$values['CANCELURL'] = $this->getConfigValue( array( 'payment.url-cancel', 'payment.url-success' ) ); |
200
|
|
|
$values['USERSELECTEDFUNDINGSOURCE'] = $this->getConfigValue( array( 'paypalexpress.FundingSource' ), 'CreditCard' ); |
201
|
|
|
$values['LANDINGPAGE'] = $this->getConfigValue( array( 'paypalexpress.LandingPage' ), 'Login' ); |
202
|
|
|
|
203
|
|
|
$urlQuery = http_build_query( $values, '', '&' ); |
204
|
|
|
$response = $this->getCommunication()->transmit( $this->apiendpoint, 'POST', $urlQuery ); |
205
|
|
|
$rvals = $this->checkResponse( $order->getId(), $response, __METHOD__ ); |
206
|
|
|
|
207
|
|
|
$default = 'https://www.paypal.com/webscr&cmd=_express-checkout&useraction=commit&token=%1$s'; |
208
|
|
|
$paypalUrl = sprintf( $this->getConfigValue( array( 'paypalexpress.PaypalUrl' ), $default ), $rvals['TOKEN'] ); |
209
|
|
|
|
210
|
|
|
$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT; |
211
|
|
|
$serviceItem = $orderBaseItem->getService( $type, $this->getServiceItem()->getCode() ); |
212
|
|
|
$this->setAttributes( $serviceItem, ['TOKEN' => $rvals['TOKEN']], 'payment/paypal' ); |
|
|
|
|
213
|
|
|
$this->saveOrderBase( $orderBaseItem ); |
214
|
|
|
|
215
|
|
|
return new \Aimeos\MShop\Common\Item\Helper\Form\Standard( $paypalUrl, 'POST', [] ); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Queries for status updates for the given order if supported. |
221
|
|
|
* |
222
|
|
|
* @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object |
223
|
|
|
*/ |
224
|
|
|
public function query( \Aimeos\MShop\Order\Item\Iface $order ) |
225
|
|
|
{ |
226
|
|
|
if( ( $tid = $this->getOrderServiceItem( $order->getBaseId() )->getAttribute( 'TRANSACTIONID', 'payment/paypal' ) ) === null ) |
227
|
|
|
{ |
228
|
|
|
$msg = sprintf( 'PayPal Express: Payment transaction ID for order ID "%1$s" not available', $order->getId() ); |
229
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
$values = $this->getAuthParameter(); |
233
|
|
|
$values['METHOD'] = 'GetTransactionDetails'; |
234
|
|
|
$values['TRANSACTIONID'] = $tid; |
235
|
|
|
|
236
|
|
|
$urlQuery = http_build_query( $values, '', '&' ); |
237
|
|
|
$response = $this->getCommunication()->transmit( $this->apiendpoint, 'POST', $urlQuery ); |
238
|
|
|
$rvals = $this->checkResponse( $order->getId(), $response, __METHOD__ ); |
239
|
|
|
|
240
|
|
|
$this->setPaymentStatus( $order, $rvals ); |
241
|
|
|
$this->saveOrder( $order ); |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Captures the money later on request for the given order if supported. |
247
|
|
|
* |
248
|
|
|
* @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object |
249
|
|
|
*/ |
250
|
|
|
public function capture( \Aimeos\MShop\Order\Item\Iface $order ) |
251
|
|
|
{ |
252
|
|
|
$baseItem = $this->getOrderBase( $order->getBaseId() ); |
253
|
|
|
$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT; |
254
|
|
|
$serviceItem = $baseItem->getService( $type, $this->getServiceItem()->getCode() ); |
255
|
|
|
|
256
|
|
|
if( ( $tid = $serviceItem->getAttribute( 'TRANSACTIONID', 'payment/paypal' ) ) === null ) |
257
|
|
|
{ |
258
|
|
|
$msg = sprintf( 'PayPal Express: Payment transaction ID for order ID "%1$s" not available', $order->getId() ); |
259
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$price = $baseItem->getPrice(); |
263
|
|
|
|
264
|
|
|
$values = $this->getAuthParameter(); |
265
|
|
|
$values['METHOD'] = 'DoCapture'; |
266
|
|
|
$values['COMPLETETYPE'] = 'Complete'; |
267
|
|
|
$values['AUTHORIZATIONID'] = $tid; |
268
|
|
|
$values['INVNUM'] = $order->getId(); |
269
|
|
|
$values['CURRENCYCODE'] = $price->getCurrencyId(); |
270
|
|
|
$values['AMT'] = $this->getAmount( $price ); |
271
|
|
|
|
272
|
|
|
$urlQuery = http_build_query( $values, '', '&' ); |
273
|
|
|
$response = $this->getCommunication()->transmit( $this->apiendpoint, 'POST', $urlQuery ); |
274
|
|
|
$rvals = $this->checkResponse( $order->getId(), $response, __METHOD__ ); |
275
|
|
|
|
276
|
|
|
$this->setPaymentStatus( $order, $rvals ); |
277
|
|
|
|
278
|
|
|
$attributes = []; |
279
|
|
|
if( isset( $rvals['PARENTTRANSACTIONID'] ) ) { |
280
|
|
|
$attributes['PARENTTRANSACTIONID'] = $rvals['PARENTTRANSACTIONID']; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
//updates the transaction id |
284
|
|
|
$attributes['TRANSACTIONID'] = $rvals['TRANSACTIONID']; |
285
|
|
|
$this->setAttributes( $serviceItem, $attributes, 'payment/paypal' ); |
|
|
|
|
286
|
|
|
|
287
|
|
|
$this->saveOrderBase( $baseItem ); |
288
|
|
|
$this->saveOrder( $order ); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Refunds the money for the given order if supported. |
294
|
|
|
* |
295
|
|
|
* @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object |
296
|
|
|
*/ |
297
|
|
|
public function refund( \Aimeos\MShop\Order\Item\Iface $order ) |
298
|
|
|
{ |
299
|
|
|
$baseItem = $this->getOrderBase( $order->getBaseId() ); |
300
|
|
|
$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT; |
301
|
|
|
$serviceItem = $baseItem->getService( $type, $this->getServiceItem()->getCode() ); |
302
|
|
|
|
303
|
|
|
if( ( $tid = $serviceItem->getAttribute( 'TRANSACTIONID', 'payment/paypal' ) ) === null ) |
304
|
|
|
{ |
305
|
|
|
$msg = sprintf( 'PayPal Express: Payment transaction ID for order ID "%1$s" not available', $order->getId() ); |
306
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
$values = $this->getAuthParameter(); |
310
|
|
|
$values['METHOD'] = 'RefundTransaction'; |
311
|
|
|
$values['REFUNDSOURCE'] = 'instant'; |
312
|
|
|
$values['REFUNDTYPE'] = 'Full'; |
313
|
|
|
$values['TRANSACTIONID'] = $tid; |
314
|
|
|
$values['INVOICEID'] = $order->getId(); |
315
|
|
|
|
316
|
|
|
$urlQuery = http_build_query( $values, '', '&' ); |
317
|
|
|
$response = $this->getCommunication()->transmit( $this->apiendpoint, 'POST', $urlQuery ); |
318
|
|
|
$rvals = $this->checkResponse( $order->getId(), $response, __METHOD__ ); |
319
|
|
|
|
320
|
|
|
$attributes = array( 'REFUNDTRANSACTIONID' => $rvals['REFUNDTRANSACTIONID'] ); |
321
|
|
|
$this->setAttributes( $serviceItem, $attributes, 'payment/paypal' ); |
|
|
|
|
322
|
|
|
$this->saveOrderBase( $baseItem ); |
323
|
|
|
|
324
|
|
|
$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_REFUND ); |
325
|
|
|
$this->saveOrder( $order ); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* Cancels the authorization for the given order if supported. |
331
|
|
|
* |
332
|
|
|
* @param \Aimeos\MShop\Order\Item\Iface $order Order invoice object |
333
|
|
|
*/ |
334
|
|
|
public function cancel( \Aimeos\MShop\Order\Item\Iface $order ) |
335
|
|
|
{ |
336
|
|
|
if( ( $tid = $this->getOrderServiceItem( $order->getBaseId() )->getAttribute( 'TRANSACTIONID', 'payment/paypal' ) ) === null ) |
337
|
|
|
{ |
338
|
|
|
$msg = sprintf( 'PayPal Express: Payment transaction ID for order ID "%1$s" not available', $order->getId() ); |
339
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
$values = $this->getAuthParameter(); |
343
|
|
|
$values['METHOD'] = 'DoVoid'; |
344
|
|
|
$values['AUTHORIZATIONID'] = $tid; |
345
|
|
|
|
346
|
|
|
$urlQuery = http_build_query( $values, '', '&' ); |
347
|
|
|
$response = $this->getCommunication()->transmit( $this->apiendpoint, 'POST', $urlQuery ); |
348
|
|
|
$this->checkResponse( $order->getId(), $response, __METHOD__ ); |
349
|
|
|
|
350
|
|
|
$order->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_CANCELED ); |
351
|
|
|
$this->saveOrder( $order ); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Updates the order status sent by payment gateway notifications |
357
|
|
|
* |
358
|
|
|
* @param \Psr\Http\Message\ServerRequestInterface Request object |
359
|
|
|
* @return \Psr\Http\Message\ResponseInterface Response object |
360
|
|
|
*/ |
361
|
|
|
public function updatePush( \Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response ) |
362
|
|
|
{ |
363
|
|
|
$params = $request->getQueryParams(); |
364
|
|
|
|
365
|
|
|
if( !isset( $params['txn_id'] ) ) { //tid from ipn |
366
|
|
|
return $response->withStatus( 400, 'PayPal Express: Parameter "txn_id" is missing' ); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
$urlQuery = http_build_query( $params, '', '&' ); |
370
|
|
|
|
371
|
|
|
//validation |
372
|
|
|
$result = $this->getCommunication()->transmit( $this->getConfigValue( array( 'paypalexpress.url-validate' ) ), 'POST', $urlQuery ); |
373
|
|
|
|
374
|
|
|
if( $result !== 'VERIFIED' ) { |
375
|
|
|
return $response->withStatus( 400, sprintf( 'PayPal Express: Invalid request "%1$s"', $urlQuery ) ); |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
|
379
|
|
|
$order = $this->getOrder( $params['invoice'] ); |
380
|
|
|
$baseItem = $this->getOrderBase( $order->getBaseId() ); |
381
|
|
|
$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT; |
382
|
|
|
$serviceItem = $baseItem->getService( $type, $this->getServiceItem()->getCode() ); |
383
|
|
|
|
384
|
|
|
$this->checkIPN( $baseItem, $params ); |
385
|
|
|
|
386
|
|
|
$status = array( 'PAYMENTSTATUS' => $params['payment_status'] ); |
387
|
|
|
|
388
|
|
|
if( isset( $params['pending_reason'] ) ) { |
389
|
|
|
$status['PENDINGREASON'] = $params['pending_reason']; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
$this->setAttributes( $serviceItem, array( $params['txn_id'] => $params['payment_status'] ), 'payment/paypal/txn' ); |
|
|
|
|
393
|
|
|
$this->setAttributes( $serviceItem, array( 'TRANSACTIONID' => $params['txn_id'] ), 'payment/paypal' ); |
|
|
|
|
394
|
|
|
$this->saveOrderBase( $baseItem ); |
395
|
|
|
|
396
|
|
|
$this->setPaymentStatus( $order, $status ); |
397
|
|
|
$this->saveOrder( $order ); |
398
|
|
|
|
399
|
|
|
return $response->withStatus( 200 ); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* Updates the orders for which status updates were received via direct requests (like HTTP). |
405
|
|
|
* |
406
|
|
|
* @param array $params Associative list of request parameters |
407
|
|
|
* @param string|null $body Information sent within the body of the request |
408
|
|
|
* @param string|null &$response Response body for notification requests |
409
|
|
|
* @param array &$header Response headers for notification requests |
410
|
|
|
* @return \Aimeos\MShop\Order\Item\Iface|null Order item if update was successful, null if the given parameters are not valid for this provider |
411
|
|
|
* @throws \Aimeos\MShop\Service\Exception If updating one of the orders failed |
412
|
|
|
*/ |
413
|
|
|
public function updateSync( array $params = [], $body = null, &$response = null, array &$header = [] ) |
414
|
|
|
{ |
415
|
|
|
if( !isset( $params['token'] ) || !isset( $params['PayerID'] ) || !isset( $params['orderid'] ) ) { |
416
|
|
|
throw new \Aimeos\MShop\Service\Exception( 'Parameter "token" "PayerID" or "orderid" missing' ); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
$order = $this->getOrder( $params['orderid'] ); |
420
|
|
|
$baseItem = $this->getOrderBase( $order->getBaseId() ); |
421
|
|
|
$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT; |
422
|
|
|
$serviceItem = $baseItem->getService( $type, $this->getServiceItem()->getCode() ); |
423
|
|
|
|
424
|
|
|
$price = $baseItem->getPrice(); |
425
|
|
|
|
426
|
|
|
$values = $this->getAuthParameter(); |
427
|
|
|
$values['METHOD'] = 'DoExpressCheckoutPayment'; |
428
|
|
|
$values['TOKEN'] = $params['token']; |
429
|
|
|
$values['PAYERID'] = $params['PayerID']; |
430
|
|
|
$values['PAYMENTACTION'] = $this->getConfigValue( array( 'paypalexpress.PaymentAction' ), 'Sale' ); |
431
|
|
|
$values['CURRENCYCODE'] = $price->getCurrencyId(); |
432
|
|
|
$values['NOTIFYURL'] = $this->getConfigValue( array( 'payment.url-update', 'payment.url-success' ) ); |
433
|
|
|
$values['AMT'] = $this->getAmount( $price ); |
434
|
|
|
|
435
|
|
|
$urlQuery = http_build_query( $values, '', '&' ); |
436
|
|
|
$response = $this->getCommunication()->transmit( $this->apiendpoint, 'POST', $urlQuery ); |
437
|
|
|
$rvals = $this->checkResponse( $order->getId(), $response, __METHOD__ ); |
438
|
|
|
|
439
|
|
|
$attributes = array( 'PAYERID' => $params['PayerID'] ); |
440
|
|
|
|
441
|
|
|
if( isset( $rvals['TRANSACTIONID'] ) ) |
442
|
|
|
{ |
443
|
|
|
$attributes['TRANSACTIONID'] = $rvals['TRANSACTIONID']; |
444
|
|
|
$this->setAttributes( $serviceItem, array( $rvals['TRANSACTIONID'] => $rvals['PAYMENTSTATUS'] ), 'payment/paypal/txn' ); |
|
|
|
|
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
$this->setAttributes( $serviceItem, $attributes, 'payment/paypal' ); |
|
|
|
|
448
|
|
|
$this->saveOrderBase( $baseItem ); |
449
|
|
|
|
450
|
|
|
$this->setPaymentStatus( $order, $rvals ); |
451
|
|
|
$this->saveOrder( $order ); |
452
|
|
|
|
453
|
|
|
return $order; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* Checks what features the payment provider implements. |
459
|
|
|
* |
460
|
|
|
* @param integer $what Constant from abstract class |
461
|
|
|
* @return boolean True if feature is available in the payment provider, false if not |
462
|
|
|
*/ |
463
|
|
|
public function isImplemented( $what ) |
464
|
|
|
{ |
465
|
|
|
switch( $what ) |
466
|
|
|
{ |
467
|
|
|
case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_CAPTURE: |
468
|
|
|
case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_QUERY: |
469
|
|
|
case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_CANCEL: |
470
|
|
|
case \Aimeos\MShop\Service\Provider\Payment\Base::FEAT_REFUND: |
471
|
|
|
return true; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
return false; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
|
478
|
|
|
/** |
479
|
|
|
* Checks the response from the payment server. |
480
|
|
|
* |
481
|
|
|
* @param string $orderid Order item ID |
482
|
|
|
* @param string $response Response from the payment provider |
483
|
|
|
* @param string $method Name of the calling method |
484
|
|
|
* @return array Associative list of key/value pairs containing the response parameters |
485
|
|
|
* @throws \Aimeos\MShop\Service\Exception If request was not successful and an error was returned |
486
|
|
|
*/ |
487
|
|
|
protected function checkResponse( $orderid, $response, $method ) |
488
|
|
|
{ |
489
|
|
|
$rvals = []; |
490
|
|
|
parse_str( $response, $rvals ); |
491
|
|
|
|
492
|
|
|
if( $rvals['ACK'] !== 'Success' ) |
493
|
|
|
{ |
494
|
|
|
$msg = 'PayPal Express: method = ' . $method . ', order ID = ' . $orderid . ', response = ' . print_r( $rvals, true ); |
495
|
|
|
$this->getContext()->getLogger()->log( $msg, \Aimeos\MW\Logger\Base::WARN, 'core/service/payment' ); |
496
|
|
|
|
497
|
|
|
if( $rvals['ACK'] !== 'SuccessWithWarning' ) |
498
|
|
|
{ |
499
|
|
|
$short = ( isset( $rvals['L_SHORTMESSAGE0'] ) ? $rvals['L_SHORTMESSAGE0'] : '<none>' ); |
500
|
|
|
$msg = sprintf( 'PayPal Express: Request for order ID "%1$s" failed with "%2$s"', $orderid, $short ); |
501
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
502
|
|
|
} |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
return $rvals; |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
|
509
|
|
|
/** |
510
|
|
|
* Checks if IPN message from paypal is valid. |
511
|
|
|
* |
512
|
|
|
* @param \Aimeos\MShop\Order\Item\Base\Iface $basket |
513
|
|
|
* @param array $params |
514
|
|
|
*/ |
515
|
|
|
protected function checkIPN( $basket, $params ) |
516
|
|
|
{ |
517
|
|
|
$attrManager = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'order/base/service/attribute' ); |
518
|
|
|
|
519
|
|
|
if( $this->getConfigValue( array( 'paypalexpress.AccountEmail' ) ) !== $params['receiver_email'] ) |
520
|
|
|
{ |
521
|
|
|
$msg = sprintf( 'PayPal Express: Wrong receiver email "%1$s"', $params['receiver_email'] ); |
522
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
523
|
|
|
} |
524
|
|
|
|
525
|
|
|
$price = $basket->getPrice(); |
526
|
|
|
|
527
|
|
|
if( $this->getAmount( $price ) != $params['payment_amount'] ) |
528
|
|
|
{ |
529
|
|
|
$msg = sprintf( 'PayPal Express: Wrong payment amount "%1$s" for order ID "%2$s"', $params['payment_amount'], $params['invoice'] ); |
530
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
531
|
|
|
} |
532
|
|
|
|
533
|
|
|
$search = $attrManager->createSearch(); |
534
|
|
|
$expr = array( |
535
|
|
|
$search->compare( '==', 'order.base.service.attribute.code', $params['txn_id'] ), |
536
|
|
|
$search->compare( '==', 'order.base.service.attribute.value', $params['payment_status'] ), |
537
|
|
|
); |
538
|
|
|
|
539
|
|
|
$search->setConditions( $search->combine( '&&', $expr ) ); |
540
|
|
|
$results = $attrManager->searchItems( $search ); |
541
|
|
|
|
542
|
|
|
if( ( $attr = reset( $results ) ) !== false ) |
543
|
|
|
{ |
544
|
|
|
$msg = sprintf( 'PayPal Express: Duplicate transaction with ID "%1$s" and status "%2$s" ', $params['txn_id'], $params['txn_status'] ); |
545
|
|
|
throw new \Aimeos\MShop\Service\Exception( $msg ); |
546
|
|
|
} |
547
|
|
|
} |
548
|
|
|
|
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* Maps the PayPal status to the appropriate payment status and sets it in the order object. |
552
|
|
|
* |
553
|
|
|
* @param \Aimeos\MShop\Order\Item\Iface $invoice Order invoice object |
554
|
|
|
* @param array $response Associative list of key/value pairs containing the PayPal response |
555
|
|
|
*/ |
556
|
|
|
protected function setPaymentStatus( \Aimeos\MShop\Order\Item\Iface $invoice, array $response ) |
557
|
|
|
{ |
558
|
|
|
if( !isset( $response['PAYMENTSTATUS'] ) ) { |
559
|
|
|
return; |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
switch( $response['PAYMENTSTATUS'] ) |
563
|
|
|
{ |
564
|
|
|
case 'Pending': |
565
|
|
|
if( isset( $response['PENDINGREASON'] ) ) |
566
|
|
|
{ |
567
|
|
|
if( $response['PENDINGREASON'] === 'authorization' ) |
568
|
|
|
{ |
569
|
|
|
$invoice->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_AUTHORIZED ); |
570
|
|
|
break; |
571
|
|
|
} |
572
|
|
|
|
573
|
|
|
$str = 'PayPal Express: order ID = ' . $invoice->getId() . ', PENDINGREASON = ' . $response['PENDINGREASON']; |
574
|
|
|
$this->getContext()->getLogger()->log( $str, \Aimeos\MW\Logger\Base::INFO, 'core/service/payment' ); |
575
|
|
|
} |
576
|
|
|
|
577
|
|
|
$invoice->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_PENDING ); |
578
|
|
|
break; |
579
|
|
|
|
580
|
|
|
case 'In-Progress': |
581
|
|
|
$invoice->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_PENDING ); |
582
|
|
|
break; |
583
|
|
|
|
584
|
|
|
case 'Completed': |
585
|
|
|
case 'Processed': |
586
|
|
|
$invoice->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_RECEIVED ); |
587
|
|
|
break; |
588
|
|
|
|
589
|
|
|
case 'Failed': |
590
|
|
|
case 'Denied': |
591
|
|
|
case 'Expired': |
592
|
|
|
$invoice->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_REFUSED ); |
593
|
|
|
break; |
594
|
|
|
|
595
|
|
|
case 'Refunded': |
596
|
|
|
case 'Partially-Refunded': |
597
|
|
|
case 'Reversed': |
598
|
|
|
$invoice->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_REFUND ); |
599
|
|
|
break; |
600
|
|
|
|
601
|
|
|
case 'Canceled-Reversal': |
602
|
|
|
case 'Voided': |
603
|
|
|
$invoice->setPaymentStatus( \Aimeos\MShop\Order\Item\Base::PAY_CANCELED ); |
604
|
|
|
break; |
605
|
|
|
|
606
|
|
|
default: |
607
|
|
|
$str = 'PayPal Express: order ID = ' . $invoice->getId() . ', response = ' . print_r( $response, true ); |
608
|
|
|
$this->getContext()->getLogger()->log( $str, \Aimeos\MW\Logger\Base::INFO, 'core/service/payment' ); |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
|
613
|
|
|
/** |
614
|
|
|
* Returns an list of order data required by PayPal. |
615
|
|
|
* |
616
|
|
|
* @param \Aimeos\MShop\Order\Item\Base\Iface $orderBase Order base item |
617
|
|
|
* @return array Associative list of key/value pairs with order data required by PayPal |
618
|
|
|
*/ |
619
|
|
|
protected function getOrderDetails( \Aimeos\MShop\Order\Item\Base\Iface $orderBase ) |
620
|
|
|
{ |
621
|
|
|
$lastPos = 0; |
622
|
|
|
$deliveryCosts = 0; |
623
|
|
|
$deliveryPrices = []; |
624
|
|
|
$values = $this->getAuthParameter(); |
625
|
|
|
|
626
|
|
|
|
627
|
|
|
if( $this->getConfigValue( 'paypalexpress.address', true ) ) |
628
|
|
|
{ |
629
|
|
|
try |
630
|
|
|
{ |
631
|
|
|
$orderAddressDelivery = $orderBase->getAddress( \Aimeos\MShop\Order\Item\Base\Address\Base::TYPE_PAYMENT ); |
632
|
|
|
|
633
|
|
|
/* setting up the address details */ |
634
|
|
|
$values['NOSHIPPING'] = $this->getConfigValue( array( 'paypalexpress.NoShipping' ), 1 ); |
635
|
|
|
$values['ADDROVERRIDE'] = $this->getConfigValue( array( 'paypalexpress.AddrOverride' ), 0 ); |
636
|
|
|
$values['PAYMENTREQUEST_0_SHIPTONAME'] = $orderAddressDelivery->getFirstName() . ' ' . $orderAddressDelivery->getLastName(); |
637
|
|
|
$values['PAYMENTREQUEST_0_SHIPTOSTREET'] = $orderAddressDelivery->getAddress1() . ' ' . $orderAddressDelivery->getAddress2() . ' ' . $orderAddressDelivery->getAddress3(); |
638
|
|
|
$values['PAYMENTREQUEST_0_SHIPTOCITY'] = $orderAddressDelivery->getCity(); |
639
|
|
|
$values['PAYMENTREQUEST_0_SHIPTOSTATE'] = $orderAddressDelivery->getState(); |
640
|
|
|
$values['PAYMENTREQUEST_0_SHIPTOCOUNTRYCODE'] = $orderAddressDelivery->getCountryId(); |
641
|
|
|
$values['PAYMENTREQUEST_0_SHIPTOZIP'] = $orderAddressDelivery->getPostal(); |
642
|
|
|
} |
643
|
|
|
catch( \Exception $e ) { ; } // If no address is available |
644
|
|
|
} |
645
|
|
|
|
646
|
|
|
|
647
|
|
|
if( $this->getConfigValue( 'paypalexpress.product', true ) ) |
648
|
|
|
{ |
649
|
|
|
foreach( $orderBase->getProducts() as $product ) |
650
|
|
|
{ |
651
|
|
|
$price = $product->getPrice(); |
652
|
|
|
$lastPos = $product->getPosition() - 1; |
653
|
|
|
|
654
|
|
|
$deliveryPrice = clone $price; |
655
|
|
|
$deliveryPrices = $this->addPrice( $deliveryPrices, $deliveryPrice->setValue( '0.00' ), $product->getQuantity() ); |
656
|
|
|
|
657
|
|
|
$values['L_PAYMENTREQUEST_0_NUMBER' . $lastPos] = $product->getId(); |
658
|
|
|
$values['L_PAYMENTREQUEST_0_NAME' . $lastPos] = $product->getName(); |
659
|
|
|
$values['L_PAYMENTREQUEST_0_QTY' . $lastPos] = $product->getQuantity(); |
660
|
|
|
$values['L_PAYMENTREQUEST_0_AMT' . $lastPos] = $this->getAmount( $price, false ); |
661
|
|
|
} |
662
|
|
|
} |
663
|
|
|
|
664
|
|
|
|
665
|
|
|
if( $this->getConfigValue( 'paypalexpress.service', true ) ) |
666
|
|
|
{ |
667
|
|
|
foreach( $orderBase->getService( 'payment' ) as $service ) |
668
|
|
|
{ |
669
|
|
|
$price = $service->getPrice(); |
670
|
|
|
|
671
|
|
|
if( ( $paymentCosts = $this->getAmount( $price ) ) > '0.00' ) |
672
|
|
|
{ |
673
|
|
|
$lastPos++; |
674
|
|
|
$values['L_PAYMENTREQUEST_0_NAME' . $lastPos] = $this->getContext()->getI18n()->dt( 'mshop', 'Payment costs' ); |
675
|
|
|
$values['L_PAYMENTREQUEST_0_QTY' . $lastPos] = '1'; |
676
|
|
|
$values['L_PAYMENTREQUEST_0_AMT' . $lastPos] = $paymentCosts; |
677
|
|
|
} |
678
|
|
|
} |
679
|
|
|
|
680
|
|
|
try |
681
|
|
|
{ |
682
|
|
|
foreach( $orderBase->getService( 'delivery' ) as $service ) |
683
|
|
|
{ |
684
|
|
|
$deliveryPrices = $this->addPrice( $deliveryPrices, $service->getPrice() ); |
685
|
|
|
|
686
|
|
|
foreach( $deliveryPrices as $priceItem ) { |
687
|
|
|
$deliveryCosts += $this->getAmount( $priceItem ); |
688
|
|
|
} |
689
|
|
|
|
690
|
|
|
$values['L_SHIPPINGOPTIONAMOUNT0'] = number_format( $deliveryCosts, 2, '.', '' ); |
691
|
|
|
$values['L_SHIPPINGOPTIONLABEL0'] = $service->getCode(); |
692
|
|
|
$values['L_SHIPPINGOPTIONNAME0'] = $service->getName(); |
693
|
|
|
$values['L_SHIPPINGOPTIONISDEFAULT0'] = 'true'; |
694
|
|
|
} |
695
|
|
|
} |
696
|
|
|
catch( \Exception $e ) { ; } // If no delivery service is available |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
|
700
|
|
|
$price = $orderBase->getPrice(); |
701
|
|
|
$amount = $this->getAmount( $price ); |
702
|
|
|
|
703
|
|
|
if( $deliveryCosts === 0 ) |
704
|
|
|
{ |
705
|
|
|
foreach( $deliveryPrices as $priceItem ) { |
706
|
|
|
$deliveryCosts += $this->getAmount( $priceItem ); |
707
|
|
|
} |
708
|
|
|
} |
709
|
|
|
|
710
|
|
|
$values['MAXAMT'] = $amount + 0.01; // possible rounding error |
711
|
|
|
$values['PAYMENTREQUEST_0_AMT'] = $amount; |
712
|
|
|
$values['PAYMENTREQUEST_0_ITEMAMT'] = number_format( $amount - $deliveryCosts, 2, '.', '' ); |
713
|
|
|
$values['PAYMENTREQUEST_0_SHIPPINGAMT'] = number_format( $deliveryCosts, 2, '.', '' ); |
714
|
|
|
$values['PAYMENTREQUEST_0_INSURANCEAMT'] = '0.00'; |
715
|
|
|
$values['PAYMENTREQUEST_0_INSURANCEOPTIONOFFERED'] = 'false'; |
716
|
|
|
$values['PAYMENTREQUEST_0_SHIPDISCAMT'] = '0.00'; |
717
|
|
|
$values['PAYMENTREQUEST_0_CURRENCYCODE'] = $orderBase->getPrice()->getCurrencyId(); |
718
|
|
|
$values['PAYMENTREQUEST_0_PAYMENTACTION'] = $this->getConfigValue( array( 'paypalexpress.PaymentAction' ), 'sale' ); |
719
|
|
|
|
720
|
|
|
return $values; |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
|
724
|
|
|
/** |
725
|
|
|
* Returns the data required for authorization against the PayPal server. |
726
|
|
|
* |
727
|
|
|
* @return array Associative list of key/value pairs containing the autorization parameters |
728
|
|
|
*/ |
729
|
|
|
protected function getAuthParameter() |
730
|
|
|
{ |
731
|
|
|
return array( |
732
|
|
|
'VERSION' => '204.0', |
733
|
|
|
'SIGNATURE' => $this->getConfigValue( array( 'paypalexpress.ApiSignature' ) ), |
734
|
|
|
'USER' => $this->getConfigValue( array( 'paypalexpress.ApiUsername' ) ), |
735
|
|
|
'PWD' => $this->getConfigValue( array( 'paypalexpress.ApiPassword' ) ), |
736
|
|
|
); |
737
|
|
|
} |
738
|
|
|
|
739
|
|
|
|
740
|
|
|
/** |
741
|
|
|
* Returns order service item for specified base ID. |
742
|
|
|
* |
743
|
|
|
* @param integer $baseid Base ID of the order |
744
|
|
|
* @return \Aimeos\MShop\Order\Item\Base\Service\Iface Order service item |
|
|
|
|
745
|
|
|
*/ |
746
|
|
|
protected function getOrderServiceItem( $baseid ) |
747
|
|
|
{ |
748
|
|
|
$type = \Aimeos\MShop\Order\Item\Base\Service\Base::TYPE_PAYMENT; |
749
|
|
|
$basket = $this->getOrderBase( $baseid, \Aimeos\MShop\Order\Item\Base\Base::PARTS_SERVICE ); |
750
|
|
|
|
751
|
|
|
return $basket->getService( $type, $this->getServiceItem()->getCode() ); |
752
|
|
|
} |
753
|
|
|
|
754
|
|
|
|
755
|
|
|
/** |
756
|
|
|
* Adds the costs to the price item with the corresponding tax rate |
757
|
|
|
* |
758
|
|
|
* @param \Aimeos\MShop\Price\Item\Iface[] $prices Associative list of tax rates as key and price items as value |
759
|
|
|
* @param \Aimeos\MShop\Price\Item\Iface $price Price item that should be added |
760
|
|
|
* @param integer $quantity Product quantity |
761
|
|
|
* @return \Aimeos\MShop\Price\Item\Iface[] Updated list of price items |
|
|
|
|
762
|
|
|
*/ |
763
|
|
|
protected function addPrice( array $prices, $price, $quantity = 1 ) |
764
|
|
|
{ |
765
|
|
|
$taxrate = $price->getTaxRate(); |
766
|
|
|
|
767
|
|
|
if( !isset( $prices[$taxrate] ) ) |
768
|
|
|
{ |
769
|
|
|
$prices[$taxrate] = \Aimeos\MShop\Factory::createManager( $this->getContext(), 'price' )->createItem(); |
770
|
|
|
$prices[$taxrate]->setTaxRate( $taxrate ); |
771
|
|
|
} |
772
|
|
|
|
773
|
|
|
$prices[$taxrate]->addItem( $price, $quantity ); |
774
|
|
|
|
775
|
|
|
return $prices; |
776
|
|
|
} |
777
|
|
|
} |