Completed
Push — master ( 48fdf8...841f48 )
by Roman
11s
created

OrderProcessor::config()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
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);
0 ignored issues
show
Bug introduced by
$order of type SilverShop\Model\Order is incompatible with the type array expected by parameter $args of SilverShop\Checkout\OrderEmailNotifier::create(). ( Ignorable by Annotation )

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

60
        $this->notifier = OrderEmailNotifier::create(/** @scrutinizer ignore-type */ $order);
Loading history...
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 = array(), $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(array('OrderID' => $this->order->ID))
151
            ->count() - 1;
152
153
        $transactionId = $this->order->Reference . ($numPayments > 0 ? "-$numPayments" : '');
154
155
        return array_merge(
156
            $customData,
157
            array(
158
                'transactionId'    => $transactionId,
159
                'firstName'        => $this->order->FirstName,
160
                'lastName'         => $this->order->Surname,
161
                'email'            => $this->order->Email,
162
                'company'          => $this->order->Company,
0 ignored issues
show
Bug Best Practice introduced by
The property Company does not exist on SilverShop\Model\Order. Since you implemented __get, consider adding a @property annotation.
Loading history...
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
                    array('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
            // recalculate order to be sure we have the correct total
220
            $this->order->calculate();
221
222
            $this->order->extend('onPayment'); //a payment has been made
223
            //place the order, if not already placed
224
            if ($this->canPlace($this->order)) {
225
                $this->placeOrder();
226
            } else {
227
                if ($this->order->Locale) {
228
                    ShopTools::install_locale($this->order->Locale);
229
                }
230
            }
231
232
            if (($this->order->GrandTotal() > 0 && $this->order->TotalOutstanding(false) <= 0)
233
                // Zero-dollar order (e.g. paid with loyalty points)
234
                || ($this->order->GrandTotal() == 0 && Order::config()->allow_zero_order_total)
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->order->GrandTotal() of type null|mixed to 0; this is ambiguous as not only 0 == 0 is true, but null == 0 is true, too. Consider using a strict comparison ===.
Loading history...
235
            ) {
236
                //set order as paid
237
                $this->order->Status = 'Paid';
0 ignored issues
show
Documentation Bug introduced by
It seems like 'Paid' of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBEnum of property $Status.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
238
                $this->order->write();
239
            }
240
        }
241
    }
242
243
    /**
244
     * Determine if an order can be placed.
245
     *
246
     * @param boolean $order
247
     */
248
    public function canPlace(Order $order)
249
    {
250
        if (!$order) {
0 ignored issues
show
introduced by
The condition ! $order can never be false.
Loading history...
251
            $this->error(_t(__CLASS__ . ".NoOrder", "Order does not exist."));
252
            return false;
253
        }
254
        //order status is applicable
255
        if (!$order->IsCart()) {
256
            $this->error(_t(__CLASS__ . ".NotCart", "Order is not a cart."));
257
            return false;
258
        }
259
        //order has products
260
        if ($order->Items()->Count() <= 0) {
261
            $this->error(_t(__CLASS__ . ".NoItems", "Order has no items."));
262
            return false;
263
        }
264
265
        return true;
266
    }
267
268
    /**
269
     * Takes an order from being a cart to awaiting payment.
270
     *
271
     * @return boolean - success/failure
272
     */
273
    public function placeOrder()
274
    {
275
        if (!$this->order) {
276
            $this->error(_t(__CLASS__ . ".NoOrderStarted", "A new order has not yet been started."));
277
            return false;
278
        }
279
        if (!$this->canPlace($this->order)) { //final cart validation
280
            return false;
281
        }
282
283
        if ($this->order->Locale) {
284
            ShopTools::install_locale($this->order->Locale);
285
        }
286
287
        // recalculate order to be sure we have the correct total
288
        $this->order->calculate();
289
290
        if (DB::get_conn()->supportsTransactions()) {
291
            DB::get_conn()->transactionStart();
292
        }
293
294
        //update status
295
        if ($this->order->TotalOutstanding(false)) {
296
            $this->order->Status = 'Unpaid';
0 ignored issues
show
Documentation Bug introduced by
It seems like 'Unpaid' of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBEnum of property $Status.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
297
        } else {
298
            $this->order->Status = 'Paid';
299
        }
300
301
        if (!$this->order->Placed) {
302
            $this->order->Placed = DBDatetime::now()->Rfc2822(); //record placed order datetime
0 ignored issues
show
Documentation Bug introduced by
It seems like SilverStripe\ORM\FieldTy...etime::now()->Rfc2822() of type string is incompatible with the declared type SilverStripe\ORM\FieldType\DBDatetime of property $Placed.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
303
            if ($request = Controller::curr()->getRequest()) {
304
                $this->order->IPAddress = $request->getIP(); //record client IP
305
            }
306
        }
307
308
        // Add an error handler that throws an exception upon error, so that we can catch errors as exceptions
309
        // in the following block.
310
        set_error_handler(
311
            function ($severity, $message, $file, $line) {
312
                throw new ErrorException($message, 0, $severity, $file, $line);
313
            },
314
            E_ALL & ~(E_STRICT | E_NOTICE)
315
        );
316
317
        try {
318
            //re-write all attributes and modifiers to make sure they are up-to-date before they can't be changed again
319
            $items = $this->order->Items();
320
            if ($items->exists()) {
321
                foreach ($items as $item) {
322
                    $item->onPlacement();
323
                    $item->write();
324
                }
325
            }
326
            $modifiers = $this->order->Modifiers();
327
            if ($modifiers->exists()) {
328
                foreach ($modifiers as $modifier) {
329
                    $modifier->write();
330
                }
331
            }
332
            //add member to order & customers group
333
            if ($member = Security::getCurrentUser()) {
334
                if (!$this->order->MemberID) {
335
                    $this->order->MemberID = $member->ID;
336
                }
337
                $cgroup = ShopConfigExtension::current()->CustomerGroup();
338
                if ($cgroup->exists()) {
339
                    $member->Groups()->add($cgroup);
340
                }
341
            }
342
            //allow decorators to do stuff when order is saved.
343
            $this->order->extend('onPlaceOrder');
344
            $this->order->write();
345
        } catch (Exception $ex) {
346
            // Rollback the transaction if an error occurred
347
            if (DB::get_conn()->supportsTransactions()) {
348
                DB::get_conn()->transactionRollback();
349
            }
350
            $this->error($ex->getMessage());
351
            return false;
352
        } finally {
353
            // restore the error handler, no matter what
354
            restore_error_handler();
355
        }
356
357
        // Everything went through fine, complete the transaction
358
        if (DB::get_conn()->supportsTransactions()) {
359
            DB::get_conn()->transactionEnd();
360
        }
361
362
        //remove from session
363
        ShoppingCart::singleton()->clear(false);
364
        /*
365
        $cart = ShoppingCart::curr();
366
        if ($cart && $cart->ID == $this->order->ID) {
367
            // clear the cart, but don't write the order in the process (order is finalized and should NOT be overwritten)
368
        }
369
        */
370
371
        //send confirmation if configured and receipt hasn't been sent
372
        if (self::config()->send_confirmation
373
            && !$this->order->ReceiptSent
374
        ) {
375
            $this->notifier->sendConfirmation();
376
        }
377
378
        //notify admin, if configured
379
        if (self::config()->send_admin_notification) {
380
            $this->notifier->sendAdminNotification();
381
        }
382
383
        // Save order reference to session
384
        OrderManipulationExtension::add_session_order($this->order);
385
386
        return true; //report success
387
    }
388
389
    /**
390
     * @return Order
391
     */
392
    public function getOrder()
393
    {
394
        return $this->order;
395
    }
396
397
    public function getError()
398
    {
399
        return $this->error;
400
    }
401
402
    protected function error($message)
403
    {
404
        $this->error = $message;
405
    }
406
}
407