Total Complexity | 105 |
Total Lines | 887 |
Duplicated Lines | 0 % |
Changes | 6 | ||
Bugs | 0 | Features | 1 |
Complex classes like Order 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 Order, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
70 | class Order extends DataObject |
||
71 | { |
||
72 | /** |
||
73 | * Status codes and what they mean: |
||
74 | * |
||
75 | * Unpaid (default): Order created but no successful payment by customer yet |
||
76 | * Query: Order not being processed yet (customer has a query, or could be out of stock) |
||
77 | * Paid: Order successfully paid for by customer |
||
78 | * Processing: Order paid for, package is currently being processed before shipping to customer |
||
79 | * Sent: Order paid for, processed for shipping, and now sent to the customer |
||
80 | * Complete: Order completed (paid and shipped). Customer assumed to have received their goods |
||
81 | * AdminCancelled: Order cancelled by the administrator |
||
82 | * MemberCancelled: Order cancelled by the customer (Member) |
||
83 | */ |
||
84 | private static $db = [ |
||
|
|||
85 | 'Total' => 'Currency', |
||
86 | 'Reference' => 'Varchar', //allow for customised order numbering schemes |
||
87 | //status |
||
88 | 'Placed' => 'Datetime', //date the order was placed (went from Cart to Order) |
||
89 | 'Paid' => 'Datetime', //no outstanding payment left |
||
90 | 'ReceiptSent' => 'Datetime', //receipt emailed to customer |
||
91 | 'Printed' => 'Datetime', |
||
92 | 'Dispatched' => 'Datetime', //products have been sent to customer |
||
93 | 'Status' => "Enum('Unpaid,Paid,Processing,Sent,Complete,AdminCancelled,MemberCancelled,Cart','Cart')", |
||
94 | //customer (for guest orders) |
||
95 | 'FirstName' => 'Varchar', |
||
96 | 'Surname' => 'Varchar', |
||
97 | 'Email' => 'Varchar', |
||
98 | 'Notes' => 'Text', |
||
99 | 'IPAddress' => 'Varchar(15)', |
||
100 | //separate shipping |
||
101 | 'SeparateBillingAddress' => 'Boolean', |
||
102 | // keep track of customer locale |
||
103 | 'Locale' => 'Locale', |
||
104 | ]; |
||
105 | |||
106 | private static $has_one = [ |
||
107 | 'Member' => Member::class, |
||
108 | 'ShippingAddress' => Address::class, |
||
109 | 'BillingAddress' => Address::class, |
||
110 | ]; |
||
111 | |||
112 | private static $has_many = [ |
||
113 | 'Items' => OrderItem::class, |
||
114 | 'Modifiers' => OrderModifier::class, |
||
115 | 'OrderStatusLogs' => OrderStatusLog::class, |
||
116 | ]; |
||
117 | |||
118 | private static $indexes = [ |
||
119 | 'Status' => true, |
||
120 | 'StatusPlacedCreated' => [ |
||
121 | 'type' => 'index', |
||
122 | 'columns' => ['Status', 'Placed', 'Created'] |
||
123 | ] |
||
124 | ]; |
||
125 | |||
126 | private static $defaults = [ |
||
127 | 'Status' => 'Cart', |
||
128 | ]; |
||
129 | |||
130 | private static $casting = [ |
||
131 | 'FullBillingAddress' => 'Text', |
||
132 | 'FullShippingAddress' => 'Text', |
||
133 | 'Total' => 'Currency', |
||
134 | 'SubTotal' => 'Currency', |
||
135 | 'TotalPaid' => 'Currency', |
||
136 | 'Shipping' => 'Currency', |
||
137 | 'TotalOutstanding' => 'Currency', |
||
138 | ]; |
||
139 | |||
140 | private static $summary_fields = [ |
||
141 | 'Reference', |
||
142 | 'Placed', |
||
143 | 'Name', |
||
144 | 'LatestEmail', |
||
145 | 'Total', |
||
146 | 'StatusI18N', |
||
147 | ]; |
||
148 | |||
149 | private static $searchable_fields = [ |
||
150 | 'Reference', |
||
151 | 'Name', |
||
152 | 'Email', |
||
153 | 'Status' => [ |
||
154 | 'filter' => 'ExactMatchFilter', |
||
155 | 'field' => CheckboxSetField::class, |
||
156 | ], |
||
157 | ]; |
||
158 | |||
159 | private static $table_name = 'SilverShop_Order'; |
||
160 | |||
161 | private static $singular_name = 'Order'; |
||
162 | |||
163 | private static $plural_name = 'Orders'; |
||
164 | |||
165 | private static $default_sort = '"Placed" DESC, "Created" DESC'; |
||
166 | |||
167 | /** |
||
168 | * Statuses for orders that have been placed. |
||
169 | * |
||
170 | * @config |
||
171 | */ |
||
172 | private static $placed_status = [ |
||
173 | 'Paid', |
||
174 | 'Unpaid', |
||
175 | 'Processing', |
||
176 | 'Sent', |
||
177 | 'Complete', |
||
178 | 'MemberCancelled', |
||
179 | 'AdminCancelled', |
||
180 | ]; |
||
181 | |||
182 | /** |
||
183 | * Statuses for which an order can be paid for |
||
184 | * |
||
185 | * @config |
||
186 | */ |
||
187 | private static $payable_status = [ |
||
188 | 'Cart', |
||
189 | 'Unpaid', |
||
190 | 'Processing', |
||
191 | 'Sent', |
||
192 | ]; |
||
193 | |||
194 | /** |
||
195 | * Statuses that shouldn't show in user account. |
||
196 | * |
||
197 | * @config |
||
198 | */ |
||
199 | private static $hidden_status = ['Cart']; |
||
200 | |||
201 | |||
202 | /** |
||
203 | * Statuses that should be logged in the Order-Status-Log |
||
204 | * |
||
205 | * @config |
||
206 | * @var array |
||
207 | */ |
||
208 | private static $log_status = []; |
||
209 | |||
210 | /** |
||
211 | * Whether or not an order can be cancelled before payment |
||
212 | * |
||
213 | * @config |
||
214 | * @var bool |
||
215 | */ |
||
216 | private static $cancel_before_payment = true; |
||
217 | |||
218 | /** |
||
219 | * Whether or not an order can be cancelled before processing |
||
220 | * |
||
221 | * @config |
||
222 | * @var bool |
||
223 | */ |
||
224 | private static $cancel_before_processing = false; |
||
225 | |||
226 | /** |
||
227 | * Whether or not an order can be cancelled before sending |
||
228 | * |
||
229 | * @config |
||
230 | * @var bool |
||
231 | */ |
||
232 | private static $cancel_before_sending = false; |
||
233 | |||
234 | /** |
||
235 | * Whether or not an order can be cancelled after sending |
||
236 | * |
||
237 | * @config |
||
238 | * @var bool |
||
239 | */ |
||
240 | private static $cancel_after_sending = false; |
||
241 | |||
242 | /** |
||
243 | * Place an order before payment processing begins |
||
244 | * |
||
245 | * @config |
||
246 | * @var boolean |
||
247 | */ |
||
248 | private static $place_before_payment = false; |
||
249 | |||
250 | /** |
||
251 | * Modifiers represent the additional charges or |
||
252 | * deductions associated to an order, such as |
||
253 | * shipping, taxes, vouchers etc. |
||
254 | * |
||
255 | * @config |
||
256 | * @var array |
||
257 | */ |
||
258 | private static $modifiers = []; |
||
259 | |||
260 | /** |
||
261 | * Rounding precision of order amounts |
||
262 | * |
||
263 | * @config |
||
264 | * @var int |
||
265 | */ |
||
266 | private static $rounding_precision = 2; |
||
267 | |||
268 | /** |
||
269 | * Minimal length (number of decimals) of order reference ids |
||
270 | * |
||
271 | * @config |
||
272 | * @var int |
||
273 | */ |
||
274 | private static $reference_id_padding = 5; |
||
275 | |||
276 | /** |
||
277 | * Will allow completion of orders with GrandTotal=0, |
||
278 | * which could be the case for orders paid with loyalty points or vouchers. |
||
279 | * Will send the "Paid" date on the order, even though no actual payment was taken. |
||
280 | * Will trigger the payment related extension points: |
||
281 | * Order->onPayment, OrderItem->onPayment, Order->onPaid. |
||
282 | * |
||
283 | * @config |
||
284 | * @var boolean |
||
285 | */ |
||
286 | private static $allow_zero_order_total = false; |
||
287 | |||
288 | /** |
||
289 | * A flag indicating that an order-status-log entry should be written |
||
290 | * |
||
291 | * @var bool |
||
292 | */ |
||
293 | protected $flagOrderStatusWrite = false; |
||
294 | |||
295 | public static function get_order_status_options() |
||
296 | { |
||
297 | $values = []; |
||
298 | foreach (singleton(Order::class)->dbObject('Status')->enumValues(false) as $value) { |
||
299 | $values[$value] = _t(__CLASS__ . '.STATUS_' . strtoupper($value), $value); |
||
300 | } |
||
301 | return $values; |
||
302 | } |
||
303 | |||
304 | /** |
||
305 | * Create CMS fields for cms viewing and editing orders |
||
306 | */ |
||
307 | public function getCMSFields() |
||
308 | { |
||
309 | $fields = FieldList::create(TabSet::create('Root', Tab::create('Main'))); |
||
310 | $fs = '<div class="field">'; |
||
311 | $fe = '</div>'; |
||
312 | $parts = [ |
||
313 | DropdownField::create('Status', $this->fieldLabel('Status'), self::get_order_status_options()), |
||
314 | LiteralField::create('Customer', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Customer') . $fe), |
||
315 | LiteralField::create('Addresses', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Addresses') . $fe), |
||
316 | LiteralField::create('Content', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Content') . $fe), |
||
317 | ]; |
||
318 | if ($this->Notes) { |
||
319 | $parts[] = LiteralField::create('Notes', $fs . $this->renderWith('SilverShop\Admin\OrderAdmin_Notes') . $fe); |
||
320 | } |
||
321 | $fields->addFieldsToTab('Root.Main', $parts); |
||
322 | |||
323 | $fields->addFieldToTab('Root.Modifiers', new GridField('Modifiers', 'Modifiers', $this->Modifiers())); |
||
324 | |||
325 | $this->extend('updateCMSFields', $fields); |
||
326 | |||
327 | if ($payments = $fields->fieldByName('Root.Payments.Payments')) { |
||
328 | $fields->removeByName('Payments'); |
||
329 | $fields->insertAfter('Content', $payments); |
||
330 | $payments->addExtraClass('order-payments'); |
||
331 | } |
||
332 | |||
333 | return $fields; |
||
334 | } |
||
335 | |||
336 | /** |
||
337 | * Augment field labels |
||
338 | */ |
||
339 | public function fieldLabels($includerelations = true) |
||
340 | { |
||
341 | $labels = parent::fieldLabels($includerelations); |
||
342 | |||
343 | $labels['Name'] = _t('SilverShop\Generic.Customer', 'Customer'); |
||
344 | $labels['LatestEmail'] = _t(__CLASS__ . '.db_Email', 'Email'); |
||
345 | $labels['StatusI18N'] = _t(__CLASS__ . '.db_Status', 'Status'); |
||
346 | |||
347 | return $labels; |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | * Adjust scafolded search context |
||
352 | * |
||
353 | * @return SearchContext the updated search context |
||
354 | */ |
||
355 | public function getDefaultSearchContext() |
||
356 | { |
||
357 | $context = parent::getDefaultSearchContext(); |
||
358 | $fields = $context->getFields(); |
||
359 | |||
360 | $validStates = self::config()->placed_status; |
||
361 | $statusOptions = array_filter(self::get_order_status_options(), function ($k) use ($validStates) { |
||
362 | return in_array($k, $validStates); |
||
363 | }, ARRAY_FILTER_USE_KEY); |
||
364 | |||
365 | $fields->push( |
||
366 | // TODO: Allow filtering by multiple statuses |
||
367 | DropdownField::create('Status', $this->fieldLabel('Status')) |
||
368 | ->setSource($statusOptions) |
||
369 | ->setHasEmptyDefault(true) |
||
370 | ); |
||
371 | |||
372 | // add date range filtering |
||
373 | $fields->insertBefore( |
||
374 | 'Status', |
||
375 | DateField::create('DateFrom', _t(__CLASS__ . '.DateFrom', 'Date from')) |
||
376 | ); |
||
377 | |||
378 | $fields->insertBefore( |
||
379 | 'Status', |
||
380 | DateField::create('DateTo', _t(__CLASS__ . '.DateTo', 'Date to')) |
||
381 | ); |
||
382 | |||
383 | // get the array, to maniplulate name, and fullname seperately |
||
384 | $filters = $context->getFilters(); |
||
385 | $filters['DateFrom'] = GreaterThanFilter::create('Placed'); |
||
386 | $filters['DateTo'] = LessThanFilter::create('Placed'); |
||
387 | |||
388 | // filter customer need to use a bunch of different sources |
||
389 | $filters['Name'] = MultiFieldPartialMatchFilter::create( |
||
390 | 'FirstName', |
||
391 | false, |
||
392 | ['SplitWords'], |
||
393 | [ |
||
394 | 'Surname', |
||
395 | 'Member.FirstName', |
||
396 | 'Member.Surname', |
||
397 | 'BillingAddress.FirstName', |
||
398 | 'BillingAddress.Surname', |
||
399 | 'ShippingAddress.FirstName', |
||
400 | 'ShippingAddress.Surname', |
||
401 | ] |
||
402 | ); |
||
403 | |||
404 | $context->setFilters($filters); |
||
405 | |||
406 | $this->extend('updateDefaultSearchContext', $context); |
||
407 | return $context; |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * Hack for swapping out relation list with OrderItemList |
||
412 | * |
||
413 | * @inheritdoc |
||
414 | */ |
||
415 | public function getComponents($componentName, $id = null) |
||
416 | { |
||
417 | $components = parent::getComponents($componentName, $id); |
||
418 | if ($componentName === 'Items' && get_class($components) !== UnsavedRelationList::class) { |
||
419 | $query = $components->dataQuery(); |
||
420 | $components = OrderItemList::create(OrderItem::class, 'OrderID'); |
||
421 | $components->setDataQuery($query); |
||
422 | $components = $components->forForeignID($this->ID); |
||
423 | } |
||
424 | return $components; |
||
425 | } |
||
426 | |||
427 | /** |
||
428 | * Returns the subtotal of the items for this order. |
||
429 | */ |
||
430 | public function SubTotal() |
||
431 | { |
||
432 | if ($this->Items()->exists()) { |
||
433 | return $this->Items()->SubTotal(); |
||
434 | } |
||
435 | |||
436 | return 0; |
||
437 | } |
||
438 | |||
439 | /** |
||
440 | * Calculate the total |
||
441 | * |
||
442 | * @return float the final total |
||
443 | */ |
||
444 | public function calculate() |
||
445 | { |
||
446 | $calculator = OrderTotalCalculator::create($this); |
||
447 | return $this->Total = $calculator->calculate(); |
||
448 | } |
||
449 | |||
450 | /** |
||
451 | * This is needed to maintain backwards compatiability with |
||
452 | * some subsystems using modifiers. eg discounts |
||
453 | */ |
||
454 | public function getModifier($className, $forcecreate = false) |
||
455 | { |
||
456 | $calculator = OrderTotalCalculator::create($this); |
||
457 | return $calculator->getModifier($className, $forcecreate); |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * Enforce rounding precision when setting total |
||
462 | */ |
||
463 | public function setTotal($val) |
||
464 | { |
||
465 | $this->setField('Total', round($val, self::$rounding_precision)); |
||
466 | } |
||
467 | |||
468 | /** |
||
469 | * Get final value of order. |
||
470 | * Retrieves value from DataObject's record array. |
||
471 | */ |
||
472 | public function Total() |
||
473 | { |
||
474 | return $this->getField('Total'); |
||
475 | } |
||
476 | |||
477 | /** |
||
478 | * Alias for Total. |
||
479 | */ |
||
480 | public function GrandTotal() |
||
481 | { |
||
482 | return $this->Total(); |
||
483 | } |
||
484 | |||
485 | /** |
||
486 | * Calculate how much is left to be paid on the order. |
||
487 | * Enforces rounding precision. |
||
488 | * |
||
489 | * Payments that have been authorized via a non-manual gateway should count towards the total paid amount. |
||
490 | * However, it's possible to exclude these by setting the $includeAuthorized parameter to false, which is |
||
491 | * useful to determine the status of the Order. Order status should only change to 'Paid' when all |
||
492 | * payments are 'Captured'. |
||
493 | * |
||
494 | * @param bool $includeAuthorized whether or not to include authorized payments (excluding manual payments) |
||
495 | * @return float |
||
496 | */ |
||
497 | public function TotalOutstanding($includeAuthorized = true) |
||
498 | { |
||
499 | return round( |
||
500 | $this->GrandTotal() - ($includeAuthorized ? $this->TotalPaidOrAuthorized() : $this->TotalPaid()), |
||
501 | self::config()->rounding_precision |
||
502 | ); |
||
503 | } |
||
504 | |||
505 | /** |
||
506 | * Get the order status. This will return a localized value if available. |
||
507 | * |
||
508 | * @return string the payment status |
||
509 | */ |
||
510 | public function getStatusI18N() |
||
511 | { |
||
512 | return _t(__CLASS__ . '.STATUS_' . strtoupper($this->Status), $this->Status); |
||
513 | } |
||
514 | |||
515 | /** |
||
516 | * Get the link for finishing order processing. |
||
517 | */ |
||
518 | public function Link() |
||
519 | { |
||
520 | $link = CheckoutPage::find_link(false, 'order', $this->ID); |
||
521 | |||
522 | if (Security::getCurrentUser()) { |
||
523 | $link = Controller::join_links(AccountPage::find_link(), 'order', $this->ID); |
||
524 | } |
||
525 | |||
526 | $this->extend('updateLink', $link); |
||
527 | |||
528 | return $link; |
||
529 | } |
||
530 | |||
531 | /** |
||
532 | * Returns TRUE if the order can be cancelled |
||
533 | * PRECONDITION: Order is in the DB. |
||
534 | * |
||
535 | * @return boolean |
||
536 | */ |
||
537 | public function canCancel($member = null) |
||
538 | { |
||
539 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
540 | if ($extended !== null) { |
||
541 | return $extended; |
||
542 | } |
||
543 | |||
544 | switch ($this->Status) { |
||
545 | case 'Unpaid' : |
||
546 | return self::config()->cancel_before_payment; |
||
547 | case 'Paid' : |
||
548 | return self::config()->cancel_before_processing; |
||
549 | case 'Processing' : |
||
550 | return self::config()->cancel_before_sending; |
||
551 | case 'Sent' : |
||
552 | case 'Complete' : |
||
553 | return self::config()->cancel_after_sending; |
||
554 | } |
||
555 | return false; |
||
556 | } |
||
557 | |||
558 | /** |
||
559 | * Check if an order can be paid for. |
||
560 | * |
||
561 | * @return boolean |
||
562 | */ |
||
563 | public function canPay($member = null) |
||
564 | { |
||
565 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
566 | if ($extended !== null) { |
||
567 | return $extended; |
||
568 | } |
||
569 | |||
570 | if (!in_array($this->Status, self::config()->payable_status)) { |
||
571 | return false; |
||
572 | } |
||
573 | if ($this->TotalOutstanding(true) > 0 && empty($this->Paid)) { |
||
574 | return true; |
||
575 | } |
||
576 | return false; |
||
577 | } |
||
578 | |||
579 | /** |
||
580 | * Prevent deleting orders. |
||
581 | * |
||
582 | * @return boolean |
||
583 | */ |
||
584 | public function canDelete($member = null) |
||
585 | { |
||
586 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
587 | if ($extended !== null) { |
||
588 | return $extended; |
||
589 | } |
||
590 | |||
591 | return false; |
||
592 | } |
||
593 | |||
594 | /** |
||
595 | * Check if an order can be viewed. |
||
596 | * |
||
597 | * @return boolean |
||
598 | */ |
||
599 | public function canView($member = null) |
||
600 | { |
||
601 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
602 | if ($extended !== null) { |
||
603 | return $extended; |
||
604 | } |
||
605 | |||
606 | return true; |
||
607 | } |
||
608 | |||
609 | /** |
||
610 | * Check if an order can be edited. |
||
611 | * |
||
612 | * @return boolean |
||
613 | */ |
||
614 | public function canEdit($member = null) |
||
615 | { |
||
616 | $extended = $this->extendedCan(__FUNCTION__, $member); |
||
617 | if ($extended !== null) { |
||
618 | return $extended; |
||
619 | } |
||
620 | |||
621 | return true; |
||
622 | } |
||
623 | |||
624 | /** |
||
625 | * Prevent standard creation of orders. |
||
626 | * |
||
627 | * @return boolean |
||
628 | */ |
||
629 | public function canCreate($member = null, $context = []) |
||
630 | { |
||
631 | $extended = $this->extendedCan(__FUNCTION__, $member, $context); |
||
632 | if ($extended !== null) { |
||
633 | return $extended; |
||
634 | } |
||
635 | |||
636 | return false; |
||
637 | } |
||
638 | |||
639 | /** |
||
640 | * Return the currency of this order. |
||
641 | * Note: this is a fixed value across the entire site. |
||
642 | * |
||
643 | * @return string |
||
644 | */ |
||
645 | public function Currency() |
||
648 | } |
||
649 | |||
650 | /** |
||
651 | * Get the latest email for this order.z |
||
652 | */ |
||
653 | public function getLatestEmail() |
||
654 | { |
||
655 | if ($this->hasMethod('overrideLatestEmail')) { |
||
656 | return $this->overrideLatestEmail(); |
||
657 | } |
||
658 | if ($this->MemberID && ($this->Member()->LastEdited > $this->LastEdited || !$this->Email)) { |
||
659 | return $this->Member()->Email; |
||
660 | } |
||
661 | return $this->getField('Email'); |
||
662 | } |
||
663 | |||
664 | /** |
||
665 | * Gets the name of the customer. |
||
666 | */ |
||
667 | public function getName() |
||
668 | { |
||
669 | $firstname = $this->FirstName ? $this->FirstName : $this->Member()->FirstName; |
||
670 | $surname = $this->FirstName ? $this->Surname : $this->Member()->Surname; |
||
671 | return implode(' ', array_filter([$firstname, $surname])); |
||
672 | } |
||
673 | |||
674 | public function getTitle() |
||
675 | { |
||
676 | return $this->Reference . ' - ' . $this->dbObject('Placed')->Nice(); |
||
677 | } |
||
678 | |||
679 | /** |
||
680 | * Get shipping address, or member default shipping address. |
||
681 | */ |
||
682 | public function getShippingAddress() |
||
683 | { |
||
684 | return $this->getAddress('Shipping'); |
||
685 | } |
||
686 | |||
687 | /** |
||
688 | * Get billing address, if marked to use seperate address, otherwise use shipping address, |
||
689 | * or the member default billing address. |
||
690 | */ |
||
691 | public function getBillingAddress() |
||
692 | { |
||
693 | if (!$this->SeparateBillingAddress && $this->ShippingAddressID === $this->BillingAddressID) { |
||
694 | return $this->getShippingAddress(); |
||
695 | } else { |
||
696 | return $this->getAddress('Billing'); |
||
697 | } |
||
698 | } |
||
699 | |||
700 | /** |
||
701 | * @param string $type - Billing or Shipping |
||
702 | * @return Address |
||
703 | * @throws \Exception |
||
704 | */ |
||
705 | protected function getAddress($type) |
||
706 | { |
||
707 | $address = $this->getComponent($type . 'Address'); |
||
708 | |||
709 | if (!$address || !$address->exists() && $this->Member()) { |
||
710 | $address = $this->Member()->{"Default${type}Address"}(); |
||
711 | } |
||
712 | |||
713 | if (empty($address->Surname) && empty($address->FirstName)) { |
||
714 | if ($member = $this->Member()) { |
||
715 | // If there's a member object, use information from the Member. |
||
716 | // The information from Order should have precendence if set though! |
||
717 | $address->FirstName = $this->FirstName ?: $member->FirstName; |
||
718 | $address->Surname = $this->Surname ?: $member->Surname; |
||
719 | } else { |
||
720 | $address->FirstName = $this->FirstName; |
||
721 | $address->Surname = $this->Surname; |
||
722 | } |
||
723 | } |
||
724 | |||
725 | return $address; |
||
726 | } |
||
727 | |||
728 | /** |
||
729 | * Check if the two addresses saved differ. |
||
730 | * |
||
731 | * @return boolean |
||
732 | */ |
||
733 | public function getAddressesDiffer() |
||
736 | } |
||
737 | |||
738 | /** |
||
739 | * Has this order been sent to the customer? |
||
740 | * (at "Sent" status). |
||
741 | * |
||
742 | * @return boolean |
||
743 | */ |
||
744 | public function IsSent() |
||
745 | { |
||
746 | return $this->Status == 'Sent'; |
||
747 | } |
||
748 | |||
749 | /** |
||
750 | * Is this order currently being processed? |
||
751 | * (at "Sent" OR "Processing" status). |
||
752 | * |
||
753 | * @return boolean |
||
754 | */ |
||
755 | public function IsProcessing() |
||
756 | { |
||
757 | return $this->IsSent() || $this->Status == 'Processing'; |
||
758 | } |
||
759 | |||
760 | /** |
||
761 | * Return whether this Order has been paid for (Status == Paid) |
||
762 | * or Status == Processing, where it's been paid for, but is |
||
763 | * currently in a processing state. |
||
764 | * |
||
765 | * @return boolean |
||
766 | */ |
||
767 | public function IsPaid() |
||
768 | { |
||
769 | return (boolean)$this->Paid || $this->Status == 'Paid'; |
||
770 | } |
||
771 | |||
772 | public function IsCart() |
||
775 | } |
||
776 | |||
777 | /** |
||
778 | * Create a unique reference identifier string for this order. |
||
779 | */ |
||
780 | public function generateReference() |
||
781 | { |
||
782 | $reference = str_pad($this->ID, self::$reference_id_padding, '0', STR_PAD_LEFT); |
||
783 | |||
784 | $this->extend('generateReference', $reference); |
||
785 | |||
786 | $candidate = $reference; |
||
787 | //prevent generating references that are the same |
||
788 | $count = 0; |
||
789 | while (Order::get()->filter('Reference', $candidate)->count() > 0) { |
||
790 | $count++; |
||
791 | $candidate = $reference . '' . $count; |
||
792 | } |
||
793 | $this->Reference = $candidate; |
||
794 | } |
||
795 | |||
796 | /** |
||
797 | * Get the reference for this order, or fall back to order ID. |
||
798 | */ |
||
799 | public function getReference() |
||
800 | { |
||
801 | return $this->getField('Reference') ? $this->getField('Reference') : $this->ID; |
||
802 | } |
||
803 | |||
804 | /** |
||
805 | * Force creating an order reference |
||
806 | */ |
||
807 | protected function onBeforeWrite() |
||
808 | { |
||
809 | parent::onBeforeWrite(); |
||
810 | if (!$this->getField('Reference') && in_array($this->Status, self::$placed_status)) { |
||
811 | $this->generateReference(); |
||
812 | } |
||
813 | |||
814 | // perform status transition |
||
815 | if ($this->isInDB() && $this->isChanged('Status')) { |
||
816 | $this->statusTransition( |
||
817 | empty($this->original['Status']) ? 'Cart' : $this->original['Status'], |
||
818 | $this->Status |
||
819 | ); |
||
820 | } |
||
821 | |||
822 | // While the order is unfinished/cart, always store the current locale with the order. |
||
823 | // We do this everytime an order is saved, because the user might change locale (language-switch). |
||
824 | if ($this->Status == 'Cart') { |
||
825 | $this->Locale = ShopTools::get_current_locale(); |
||
826 | } |
||
827 | } |
||
828 | |||
829 | /** |
||
830 | * Called from @see onBeforeWrite whenever status changes |
||
831 | * |
||
832 | * @param string $fromStatus status to transition away from |
||
833 | * @param string $toStatus target status |
||
834 | */ |
||
835 | protected function statusTransition($fromStatus, $toStatus) |
||
857 | } |
||
858 | } |
||
859 | |||
860 | /** |
||
861 | * delete attributes, statuslogs, and payments |
||
862 | */ |
||
863 | protected function onBeforeDelete() |
||
864 | { |
||
865 | foreach ($this->Items() as $item) { |
||
866 | $item->delete(); |
||
867 | } |
||
868 | |||
869 | foreach ($this->Modifiers() as $modifier) { |
||
870 | $modifier->delete(); |
||
871 | } |
||
872 | |||
873 | foreach ($this->OrderStatusLogs() as $logEntry) { |
||
874 | $logEntry->delete(); |
||
875 | } |
||
876 | |||
877 | // just remove the payment relations… |
||
878 | // that way payment objects still persist (they might be relevant for book-keeping?) |
||
879 | $this->Payments()->removeAll(); |
||
880 | |||
881 | parent::onBeforeDelete(); |
||
882 | } |
||
883 | |||
884 | public function onAfterWrite() |
||
885 | { |
||
886 | parent::onAfterWrite(); |
||
887 | |||
888 | //create an OrderStatusLog |
||
889 | if ($this->flagOrderStatusWrite) { |
||
890 | $this->flagOrderStatusWrite = false; |
||
891 | $log = OrderStatusLog::create(); |
||
892 | |||
893 | // populate OrderStatusLog |
||
894 | $log->Title = _t( |
||
895 | 'SilverShop\ShopEmail.StatusChanged', |
||
896 | 'Status for order #{OrderNo} changed to "{OrderStatus}"', |
||
897 | '', |
||
898 | ['OrderNo' => $this->Reference, 'OrderStatus' => $this->getStatusI18N()] |
||
899 | ); |
||
900 | $log->Note = _t('SilverShop\ShopEmail.StatusChange' . $this->Status . 'Note', $this->Status . 'Note'); |
||
901 | $log->OrderID = $this->ID; |
||
902 | OrderEmailNotifier::create($this)->sendStatusChange($log->Title, $log->Note); |
||
903 | $log->SentToCustomer = true; // Explicitly set because sendStatusChange() won't set it in this case |
||
904 | $log->VisibleToCustomer = true; |
||
905 | $this->extend('updateOrderStatusLog', $log); |
||
906 | $log->write(); |
||
907 | } |
||
908 | } |
||
909 | |||
910 | public function debug() |
||
936 | } |
||
937 | |||
938 | /** |
||
939 | * Provide i18n entities for the order class |
||
940 | * |
||
941 | * @return array |
||
942 | */ |
||
943 | public function provideI18nEntities() |
||
957 | } |
||
958 | } |
||
959 |