OrderProcessor::createPayment()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 17
nc 3
nop 1
dl 0
loc 24
rs 9.7
c 0
b 0
f 0
1
<?php
2
3
namespace SilverShop\Checkout;
4
5
use ErrorException;
6
use Exception;
7
use SilverShop\Cart\ShoppingCart;
8
use SilverShop\Extension\OrderManipulationExtension;
9
use SilverShop\Extension\ShopConfigExtension;
10
use SilverShop\Model\Order;
11
use SilverShop\ShopTools;
12
use SilverStripe\Control\Controller;
13
use SilverStripe\Core\Config\Config_ForClass;
14
use SilverStripe\Core\Config\Configurable;
15
use SilverStripe\Core\Injector\Injectable;
16
use SilverStripe\Omnipay\GatewayInfo;
17
use SilverStripe\Omnipay\Model\Payment;
18
use SilverStripe\Omnipay\Service\ServiceFactory;
19
use SilverStripe\Omnipay\Service\ServiceResponse;
20
use SilverStripe\ORM\DB;
21
use SilverStripe\ORM\FieldType\DBDatetime;
22
use SilverStripe\Security\Member;
23
use SilverStripe\Security\Security;
24
25
/**
26
 * Handles tasks to be performed on orders, particularly placing and processing/fulfilment.
27
 * Placing, Emailing Reciepts, Status Updates, Printing, Payments - things you do with a completed order.
28
 *
29
 * @package shop
30
 */
31
class OrderProcessor
32
{
33
    use Injectable;
34
    use Configurable;
35
36
    /**
37
     * @var Order
38
     */
39
    protected $order;
40
41
    /**
42
     * @var OrderEmailNotifier
43
     */
44
    protected $notifier;
45
46
    /**
47
     * @var string
48
     */
49
    protected $error;
50
51
52
    /**
53
     * Assign the order to a local variable
54
     *
55
     * @param Order $order
56
     */
57
    public function __construct(Order $order)
58
    {
59
        $this->order = $order;
60
        $this->notifier = OrderEmailNotifier::create($order);
61
    }
62
63
    /**
64
     * URL to display success message to the user.
65
     * Happens after any potential offsite gateway redirects.
66
     *
67
     * @return String Relative URL
68
     */
69
    public function getReturnUrl()
70
    {
71
        return $this->order->Link();
72
    }
73
74
    /**
75
     * Create a payment model, and provide link to redirect to external gateway,
76
     * or redirect to order link.
77
     *
78
     * @param string $gateway the gateway to use
79
     * @param array $gatewaydata the data that should be passed to the gateway
80
     * @param string $successUrl (optional) return URL for successful payments.
81
     *                            If left blank, the default return URL will be
82
     *                            used @see getReturnUrl
83
     * @param string $cancelUrl (optional) return URL for cancelled/failed payments
84
     *
85
     * @return ServiceResponse|null
86
     * @throws \SilverStripe\Omnipay\Exception\InvalidConfigurationException
87
     */
88
    public function makePayment($gateway, $gatewaydata = [], $successUrl = null, $cancelUrl = null)
89
    {
90
        //create payment
91
        $payment = $this->createPayment($gateway);
92
        if (!$payment) {
93
            //errors have been stored.
94
            return null;
95
        }
96
97
        $payment->setSuccessUrl($successUrl ? $successUrl : $this->getReturnUrl());
98
99
        // Explicitly set the cancel URL
100
        if ($cancelUrl) {
101
            $payment->setFailureUrl($cancelUrl);
102
        }
103
104
        // Create a payment service, by using the Service Factory. This will automatically choose an
105
        // AuthorizeService or PurchaseService, depending on Gateway configuration.
106
        // Set the user-facing success URL for redirects
107
        /**
108
         * @var ServiceFactory $factory
109
         */
110
        $factory = ServiceFactory::create();
111
        $service = $factory->getService($payment, ServiceFactory::INTENT_PAYMENT);
112
113
        // Initiate payment, get the result back
114
        try {
115
            $serviceResponse = $service->initiate($this->getGatewayData($gatewaydata));
116
        } catch (\SilverStripe\Omnipay\Exception\Exception $ex) {
117
            // error out when an exception occurs
118
            $this->error($ex->getMessage());
119
            return null;
120
        }
121
122
        // Check if the service response itself contains an error
123
        if ($serviceResponse->isError()) {
124
            if ($opResponse = $serviceResponse->getOmnipayResponse()) {
125
                $this->error($opResponse->getMessage());
126
            } else {
127
                $this->error('An unspecified payment error occurred. Please check the payment messages.');
128
            }
129
        }
130
131
        // For an OFFSITE payment, serviceResponse will now contain a redirect
132
        // For an ONSITE payment, ShopPayment::onCaptured will have been called, which will have called completePayment
133
134
        return $serviceResponse;
135
    }
136
137
    /**
138
     * Map shop data to omnipay fields
139
     *
140
     * @param array $customData Usually user submitted data.
141
     *
142
     * @return array
143
     */
144
    protected function getGatewayData($customData)
145
    {
146
        $shipping = $this->order->getShippingAddress();
147
        $billing = $this->order->getBillingAddress();
148
149
        $numPayments = Payment::get()
150
            ->filter(['OrderID' => $this->order->ID])
151
            ->count() - 1;
152
153
        $transactionId = $this->order->Reference . ($numPayments > 0 ? "-$numPayments" : '');
154
155
        return array_merge(
156
            $customData,
157
            [
158
                'transactionId'    => $transactionId,
159
                'firstName'        => $this->order->FirstName,
160
                'lastName'         => $this->order->Surname,
161
                'email'            => $this->order->Email,
162
                'company'          => $billing->Company,
163
                'billingAddress1'  => $billing->Address,
164
                'billingAddress2'  => $billing->AddressLine2,
165
                'billingCity'      => $billing->City,
166
                'billingPostcode'  => $billing->PostalCode,
167
                'billingState'     => $billing->State,
168
                'billingCountry'   => $billing->Country,
169
                'billingPhone'     => $billing->Phone,
170
                'shippingAddress1' => $shipping->Address,
171
                'shippingAddress2' => $shipping->AddressLine2,
172
                'shippingCity'     => $shipping->City,
173
                'shippingPostcode' => $shipping->PostalCode,
174
                'shippingState'    => $shipping->State,
175
                'shippingCountry'  => $shipping->Country,
176
                'shippingPhone'    => $shipping->Phone,
177
            ]
178
        );
179
    }
180
181
    /**
182
     * Create a new payment for an order
183
     */
184
    public function createPayment($gateway)
185
    {
186
        if (!GatewayInfo::isSupported($gateway)) {
187
            $this->error(
188
                _t(
189
                    __CLASS__ . ".InvalidGateway",
190
                    "`{gateway}` isn't a valid payment gateway.",
191
                    'gateway is the name of the payment gateway',
192
                    ['gateway' => $gateway]
193
                )
194
            );
195
            return false;
196
        }
197
        if (!$this->order->canPay(Security::getCurrentUser())) {
198
            $this->error(_t(__CLASS__ . ".CantPay", "Order can't be paid for."));
199
            return false;
200
        }
201
        $payment = Payment::create()->init(
202
            $gateway,
203
            $this->order->TotalOutstanding(true),
204
            ShopConfigExtension::config()->base_currency
205
        );
206
        $this->order->Payments()->add($payment);
0 ignored issues
show
Bug introduced by
The method Payments() does not exist on SilverShop\Model\Order. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

206
        $this->order->/** @scrutinizer ignore-call */ 
207
                      Payments()->add($payment);
Loading history...
207
        return $payment;
208
    }
209
210
    /**
211
     * Complete payment processing
212
     *    - send receipt
213
     *    - update order status accordingling
214
     *    - fire event hooks
215
     */
216
    public function completePayment()
217
    {
218
        if (!$this->order->IsPaid()) {
219
            $this->order->extend('onPayment'); //a payment has been made
220
            //place the order, if not already placed
221
            if ($this->canPlace($this->order)) {
222
                $this->placeOrder();
223
            } else {
224
                if ($this->order->Locale) {
225
                    ShopTools::install_locale($this->order->Locale);
226
                }
227
            }
228
229
            if (($this->order->GrandTotal() > 0 && $this->order->TotalOutstanding(false) <= 0)
230
                // Zero-dollar order (e.g. paid with loyalty points)
231
                || ($this->order->GrandTotal() == 0 && Order::config()->allow_zero_order_total)
232
            ) {
233
                //set order as paid
234
                $this->order->setField('Status', 'Paid');
235
                $this->order->write();
236
            }
237
        }
238
    }
239
240
    /**
241
     * Determine if an order can be placed.
242
     *
243
     * @param boolean $order
244
     */
245
    public function canPlace(Order $order)
246
    {
247
        if (!$order) {
0 ignored issues
show
introduced by
$order is of type SilverShop\Model\Order, thus it always evaluated to true.
Loading history...
248
            $this->error(_t(__CLASS__ . ".NoOrder", "Order does not exist."));
249
            return false;
250
        }
251
        //order status is applicable
252
        if (!$order->IsCart()) {
253
            $this->error(_t(__CLASS__ . ".NotCart", "Order is not a cart."));
254
            return false;
255
        }
256
        //order has products
257
        if ($order->Items()->Count() <= 0) {
258
            $this->error(_t(__CLASS__ . ".NoItems", "Order has no items."));
259
            return false;
260
        }
261
262
        return true;
263
    }
264
265
    /**
266
     * Takes an order from being a cart to awaiting payment.
267
     *
268
     * @return boolean - success/failure
269
     */
270
    public function placeOrder()
271
    {
272
        if (!$this->order) {
273
            $this->error(_t(__CLASS__ . ".NoOrderStarted", "A new order has not yet been started."));
274
            return false;
275
        }
276
        if (!$this->canPlace($this->order)) { //final cart validation
277
            return false;
278
        }
279
280
        if ($this->order->Locale) {
281
            ShopTools::install_locale($this->order->Locale);
282
        }
283
284
        if (DB::get_conn()->supportsTransactions()) {
285
            DB::get_conn()->transactionStart();
286
        }
287
288
        //update status
289
        if ($this->order->TotalOutstanding(false)) {
290
            $this->order->setField('Status', 'Unpaid');
291
        } else {
292
            $this->order->setField('Status', 'Paid');
293
        }
294
295
        if (!$this->order->Placed) {
296
            $this->order->setField('Placed', DBDatetime::now()->Rfc2822()); //record placed order datetime
297
            if ($request = Controller::curr()->getRequest()) {
298
                $this->order->IPAddress = $request->getIP(); //record client IP
299
            }
300
        }
301
302
        // Add an error handler that throws an exception upon error, so that we can catch errors as exceptions
303
        // in the following block.
304
        set_error_handler(
305
            function ($severity, $message, $file, $line) {
306
                if (!(error_reporting() & $severity)) {
307
                    // suppressed error, for example from exif_read_data in image manipulation
308
                    return false;
309
                }
310
                throw new ErrorException($message, 0, $severity, $file, $line);
311
            },
312
            E_ALL & ~(E_STRICT | E_NOTICE | E_DEPRECATED | E_USER_DEPRECATED)
313
        );
314
315
        try {
316
            //re-write all attributes and modifiers to make sure they are up-to-date before they can't be changed again
317
            $items = $this->order->Items();
318
            if ($items->exists()) {
319
                foreach ($items as $item) {
320
                    $item->onPlacement();
321
                    $item->write();
322
                }
323
            }
324
            $modifiers = $this->order->Modifiers();
325
            if ($modifiers->exists()) {
326
                foreach ($modifiers as $modifier) {
327
                    $modifier->write();
328
                }
329
            }
330
            //add member to order & customers group
331
            if ($member = Security::getCurrentUser()) {
332
                if (!$this->order->MemberID) {
333
                    $this->order->MemberID = $member->ID;
334
                }
335
                $cgroup = ShopConfigExtension::current()->CustomerGroup();
336
                if ($cgroup->exists()) {
337
                    $member->Groups()->add($cgroup);
338
                }
339
            }
340
            //allow decorators to do stuff when order is saved.
341
            $this->order->extend('onPlaceOrder');
342
            $this->order->write();
343
        } catch (Exception $ex) {
344
            // Rollback the transaction if an error occurred
345
            if (DB::get_conn()->supportsTransactions()) {
346
                DB::get_conn()->transactionRollback();
347
            }
348
            $this->error($ex->getMessage());
349
            return false;
350
        } finally {
351
            // restore the error handler, no matter what
352
            restore_error_handler();
353
        }
354
355
        // Everything went through fine, complete the transaction
356
        if (DB::get_conn()->supportsTransactions()) {
357
            DB::get_conn()->transactionEnd();
358
        }
359
360
        //remove from session
361
        ShoppingCart::singleton()->clear(false);
362
        /*
363
        $cart = ShoppingCart::curr();
364
        if ($cart && $cart->ID == $this->order->ID) {
365
            // clear the cart, but don't write the order in the process (order is finalized and should NOT be overwritten)
366
        }
367
        */
368
369
        //send confirmation if configured and receipt hasn't been sent
370
        if (self::config()->send_confirmation
371
            && !$this->order->ReceiptSent
372
        ) {
373
            $this->notifier->sendConfirmation();
374
        }
375
376
        //notify admin, if configured
377
        if (self::config()->send_admin_notification) {
378
            $this->notifier->sendAdminNotification();
379
        }
380
381
        // Save order reference to session
382
        OrderManipulationExtension::add_session_order($this->order);
383
384
        return true; //report success
385
    }
386
387
    /**
388
     * @return Order
389
     */
390
    public function getOrder()
391
    {
392
        return $this->order;
393
    }
394
395
    public function getError()
396
    {
397
        return $this->error;
398
    }
399
400
    protected function error($message)
401
    {
402
        $this->error = $message;
403
    }
404
}
405