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