Completed
Push — master ( 2eee10...777fce )
by Nicolaas
03:39
created

code/model/address/OrderAddress.php (6 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
4
/**
5
 * @description: each order has an address: a Shipping and a Billing address
6
 * This is a base-class for both.
7
 *
8
 *
9
 * @authors: Nicolaas [at] Sunny Side Up .co.nz
10
 * @package: ecommerce
11
 * @sub-package: address
12
 * @inspiration: Silverstripe Ltd, Jeremy
13
 **/
14
class OrderAddress extends DataObject implements EditableEcommerceObject
15
{
16
    /**
17
     * standard SS static definition.
18
     */
19
    private static $singular_name = 'Order Address';
20
    public function i18n_singular_name()
21
    {
22
        return _t('OrderAddress.ORDERADDRESS', 'Order Address');
23
    }
24
25
    /**
26
     * standard SS static definition.
27
     */
28
    private static $plural_name = 'Order Addresses';
29
    public function i18n_plural_name()
30
    {
31
        return _t('OrderAddress.ORDERADDRESSES', 'Order Addresses');
32
    }
33
34
    /**
35
     * standard SS static definition.
36
     */
37
    private static $casting = array(
38
        'FullName' => 'Text',
39
        'FullString' => 'Text',
40
        'JSONData' => 'Text',
41
    );
42
43
    /**
44
     * returns the id of the MAIN country field for template manipulation.
45
     * Main means the one that is used as the primary one (e.g. for tax purposes).
46
     *
47
     * @see EcommerceConfig::get("OrderAddress", "use_shipping_address_for_main_region_and_country")
48
     *
49
     * @return string
50
     */
51
    public static function get_country_field_ID()
52
    {
53
        if (EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country')) {
54
            return 'ShippingCountry';
55
        } else {
56
            return 'Country';
57
        }
58
    }
59
60
    /**
61
     * returns the id of the MAIN region field for template manipulation.
62
     * Main means the one that is used as the primary one (e.g. for tax purposes).
63
     *
64
     * @return string
65
     */
66
    public static function get_region_field_ID()
67
    {
68
        if (EcommerceConfig::get('OrderAddress', 'use_shipping_address_for_main_region_and_country')) {
69
            return 'ShippingRegion';
70
        } else {
71
            return 'Region';
72
        }
73
    }
74
75
    /**
76
     * There might be times when a modifier needs to make an address field read-only.
77
     * In that case, this is done here.
78
     *
79
     * @var array
80
     */
81
    protected $readOnlyFields = array();
82
83
    /**
84
     * sets a field to readonly state
85
     * we use this when modifiers have been set that require a field to be a certain value
86
     * for example - a PostalCode field maybe set in the modifier.
87
     *
88
     * @param string $fieldName
89
     */
90
    public function addReadOnlyField($fieldName)
91
    {
92
        $this->readOnlyFields[$fieldName] = $fieldName;
93
    }
94
95
    /**
96
     * removes a field from the readonly state.
97
     *
98
     * @param string $fieldName
99
     */
100
    public function removeReadOnlyField($fieldName)
101
    {
102
        unset($this->readOnlyFields[$fieldName]);
103
    }
104
105
    /**
106
     * link to edit the record.
107
     *
108
     * @param string | Null $action - e.g. edit
109
     *
110
     * @return string
111
     */
112
    public function CMSEditLink($action = null)
113
    {
114
        return Controller::join_links(
115
            Director::baseURL(),
116
            '/admin/sales/'.$this->ClassName.'/EditForm/field/'.$this->ClassName.'/item/'.$this->ID.'/',
117
            $action
118
        );
119
    }
120
121
    /**
122
     * save edit status for speed's sake.
123
     *
124
     * @var bool
125
     */
126
    protected $_canEdit = null;
127
128
    /**
129
     * save view status for speed's sake.
130
     *
131
     * @var bool
132
     */
133
    protected $_canView = null;
134
135
    public function canCreate($member = null)
136
    {
137
        if (! $member) {
138
            $member = Member::currentUser();
139
        }
140
        $extended = $this->extendedCan(__FUNCTION__, $member);
141
        if ($extended !== null) {
142
            return $extended;
143
        }
144
        if (Permission::checkMember($member, Config::inst()->get('EcommerceRole', 'admin_permission_code'))) {
145
            return true;
146
        }
147
148
        return parent::canEdit($member);
149
    }
150
151
    /**
152
     * Standard SS method
153
     * This is an important method.
154
     *
155
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
156
     *
157
     * @return bool
0 ignored issues
show
Should the return type not be boolean|string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
158
     **/
159
    public function canView($member = null)
160
    {
161
        if (! $member) {
162
            $member = Member::currentUser();
163
        }
164
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
165
        if ($extended !== null) {
166
            return $extended;
167
        }
168
        if (!$this->exists()) {
169
            return $this->canCreate($member);
170
        }
171
        if ($this->_canView === null) {
172
            $this->_canView = false;
173
            if ($this->Order()) {
174
                if ($this->Order()->exists()) {
175
                    if ($this->Order()->canView($member)) {
176
                        $this->_canView = true;
177
                    }
178
                }
179
            }
180
        }
181
182
        return $this->_canView;
183
    }
184
185
    /**
186
     * Standard SS method
187
     * This is an important method.
188
     *
189
     * @param Member $member
0 ignored issues
show
Should the type for parameter $member not be Member|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
190
     *
191
     * @return bool
0 ignored issues
show
Should the return type not be boolean|string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
192
     **/
193
    public function canEdit($member = null)
194
    {
195
        if (! $member) {
196
            $member = Member::currentUser();
197
        }
198
        $extended = $this->extendedCan(__FUNCTION__, $member);
0 ignored issues
show
$member is of type object<DataObject>|null, but the function expects a object<Member>|integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
199
        if ($extended !== null) {
200
            return $extended;
201
        }
202
        if (! $this->exists()) {
203
            return $this->canCreate($member);
204
        }
205
        if ($this->_canEdit === null) {
206
            $this->_canEdit = false;
207
            if ($this->Order()) {
208
                if ($this->Order()->exists()) {
209
                    if ($this->Order()->canEdit($member)) {
210
                        if (! $this->Order()->IsSubmitted()) {
211
                            $this->_canEdit = true;
212
                        }
213
                    }
214
                }
215
            }
216
        }
217
218
        return $this->_canEdit;
219
    }
220
221
    public function canDelete($member = null)
222
    {
223
        if (! $member) {
224
            $member = Member::currentUser();
225
        }
226
        $extended = $this->extendedCan(__FUNCTION__, $member);
227
        if ($extended !== null) {
228
            return $extended;
229
        }
230
231
        return false;
232
    }
233
234
    /**
235
     * Determine which properties on the DataObject are
236
     * searchable, and map them to their default {@link FormField}
237
     * representations. Used for scaffolding a searchform for {@link ModelAdmin}.
238
     *
239
     * Some additional logic is included for switching field labels, based on
240
     * how generic or specific the field type is.
241
     *
242
     * Used by {@link SearchContext}.
243
     *
244
     * @param array $_params
245
     *                       'fieldClasses': Associative array of field names as keys and FormField classes as values
246
     *                       'restrictFields': Numeric array of a field name whitelist
247
     *
248
     * @return FieldList
249
     */
250
    public function scaffoldSearchFields($_params = null)
251
    {
252
        $fields = parent::scaffoldSearchFields($_params);
253
        $fields = parent::scaffoldSearchFields();
254
        $fields->replaceField('OrderID', new NumericField('OrderID', 'Order Number'));
255
256
        return $fields;
257
    }
258
259
    /**
260
     * @return FieldList
261
     */
262
    protected function getEcommerceFields()
263
    {
264
        return new FieldList();
265
    }
266
267
    /**
268
     * put together a textfield for a postal code field.
269
     *
270
     * @param string $name - name of the field
271
     *
272
     * @return TextField
273
     **/
274
    protected function getPostalCodeField($name)
275
    {
276
        $field = new TextField($name, _t('OrderAddress.POSTALCODE', 'Postal Code'));
277
        $postalCodeURL = EcommerceDBConfig::current_ecommerce_db_config()->PostalCodeURL;
278
        $postalCodeLabel = EcommerceDBConfig::current_ecommerce_db_config()->PostalCodeLabel;
279
        if ($postalCodeURL && $postalCodeLabel) {
280
            $prefix = EcommerceConfig::get('OrderAddress', 'field_class_and_id_prefix');
281
            $field->setRightTitle('<a href="'.$postalCodeURL.'" id="'.$prefix.$name.'Link" class="'.$prefix.'postalCodeLink">'.$postalCodeLabel.'</a>');
282
        }
283
284
        return $field;
285
    }
286
287
    /**
288
     * put together a dropdown for the region field.
289
     *
290
     * @param string $name - name of the field
291
     *
292
     * @return DropdownField
293
     **/
294
    protected function getRegionField($name, $freeTextName = '')
295
    {
296
        if (EcommerceRegion::show()) {
297
            $nameWithoutID = str_replace('ID', '', $name);
298
            $title = _t('OrderAddress.'.strtoupper($nameWithoutID), 'Region / Province / State');
299
            $regionsForDropdown = EcommerceRegion::list_of_allowed_entries_for_dropdown();
300
            $count = count($regionsForDropdown);
301
            if ($count < 1) {
302
                if (!$freeTextName) {
303
                    $freeTextName = $nameWithoutID.'Code';
304
                }
305
                $regionField = new TextField($freeTextName, $title);
306
            } else {
307
                $regionField = new DropdownField($name, $title, $regionsForDropdown);
308
                if ($count < 2) {
309
                    //readonly shows as number (ID), rather than title
310
                    //$regionField = $regionField->performReadonlyTransformation();
311
                } else {
312
                    $regionField->setEmptyString(_t('OrderAdress.PLEASE_SELECT_REGION', '--- Select Region ---'));
313
                }
314
            }
315
        } else {
316
            //adding region field here as hidden field to make the code easier below...
317
            $regionField = new HiddenField($name, '', 0);
318
        }
319
        $prefix = EcommerceConfig::get('OrderAddress', 'field_class_and_id_prefix');
320
        $regionField->addExtraClass($prefix.'ajaxRegionField');
321
322
        return $regionField;
323
    }
324
325
    /**
326
     * put together a dropdown for the country field.
327
     *
328
     * @param string $name - name of the field
329
     *
330
     * @return DropdownField
331
     **/
332
    protected function getCountryField($name)
333
    {
334
        $countriesForDropdown = EcommerceCountry::list_of_allowed_entries_for_dropdown();
335
        $title = _t('OrderAddress.'.strtoupper($name), 'Country');
336
        $countryField = new DropdownField($name, $title, $countriesForDropdown, EcommerceCountry::get_country(false, $this->OrderID));
337
        $countryField->setRightTitle(_t('OrderAddress.'.strtoupper($name).'_RIGHT', ''));
338
        if (count($countriesForDropdown) < 2) {
339
            $countryField = $countryField->performReadonlyTransformation();
340
            if (count($countriesForDropdown) < 1) {
341
                $countryField = new HiddenField($name, '', 'not available');
342
            }
343
        }
344
        $prefix = EcommerceConfig::get('OrderAddress', 'field_class_and_id_prefix');
345
        $countryField->addExtraClass($prefix.'ajaxCountryField');
346
        //important, otherwise loadData will override the default value....
347
        $this->$name = EcommerceCountry::get_country(false, $this->OrderID);
348
349
        return $countryField;
350
    }
351
352
    /**
353
     * makes selected fields into read only using the $this->readOnlyFields array.
354
     *
355
     * @param FieldList | Composite $fields
356
     *
357
     * @return FieldList
358
     */
359
    protected function makeSelectedFieldsReadOnly($fields)
360
    {
361
        $this->extend('augmentMakeSelectedFieldsReadOnly', $fields);
362
        if (is_array($this->readOnlyFields) && count($this->readOnlyFields)) {
363
            foreach ($this->readOnlyFields as $readOnlyField) {
364
                if ($oldField = $fields->fieldByName($readOnlyField)) {
365
                    $fields->replaceField($readOnlyField, $oldField->performReadonlyTransformation());
366
                }
367
            }
368
        }
369
370
        return $fields;
371
    }
372
373
    /**
374
     * Saves region - both shipping and billing fields are saved here for convenience sake (only one actually gets saved)
375
     * NOTE: do not call this method SetCountry as this has a special meaning! *.
376
     *
377
     * @param int -  RegionID
378
     **/
379
    public function SetRegionFields($regionID)
380
    {
381
        $regionField = $this->fieldPrefix().'RegionID';
382
        $this->$regionField = $regionID;
383
        $this->write();
384
    }
385
386
    /**
387
     * Saves country - both shipping and billing fields are saved here for convenience sake (only one actually gets saved)
388
     * NOTE: do not call this method SetCountry as this has a special meaning!
389
     *
390
     * @param string - CountryCode - e.g. NZ
391
     */
392
    public function SetCountryFields($countryCode)
393
    {
394
        $countryField = $this->fieldPrefix().'Country';
395
        $this->$countryField = $countryCode;
396
        $this->write();
397
    }
398
399
    /**
400
     * Casted variable
401
     * returns the full name of the person, e.g. "John Smith".
402
     *
403
     * @return string
404
     */
405
    public function getFullName()
406
    {
407
        $fieldNameField = $this->fieldPrefix().'FirstName';
408
        $fieldFirst = $this->$fieldNameField;
409
        $lastNameField = $this->fieldPrefix().'Surname';
410
        $fieldLast = $this->$lastNameField;
411
412
        return $fieldFirst.' '.$fieldLast;
413
    }
414
    public function FullName()
415
    {
416
        return $this->getFullName();
417
    }
418
419
    /**
420
     * Casted variable
421
     * returns the full strng of the record.
422
     *
423
     * @return string
424
     */
425
    public function FullString()
426
    {
427
        return $this->getFullString();
428
    }
429
    public function getFullString()
430
    {
431
        Config::nest();
432
        Config::inst()->update('SSViewer', 'theme_enabled', true);
433
        $html = $this->renderWith('Order_Address'.str_replace('Address', '', $this->ClassName).'FullString');
434
        Config::unnest();
435
436
        return $html;
437
    }
438
439
    /**
440
     * returns a string that can be used to find out if two addresses are the same.
441
     *
442
     * @return string
443
     */
444
    public function comparisonString()
445
    {
446
        $comparisonString = '';
447
        $excludedFields = array('ID', 'OrderID');
448
        $fields = $this->stat('db');
449
        $regionFieldName = $this->fieldPrefix().'RegionID';
450
        $fields[$regionFieldName] = $regionFieldName;
451
        if ($fields) {
452
            foreach ($fields as $field => $useless) {
453
                if (!in_array($field, $excludedFields)) {
454
                    $comparisonString .= preg_replace('/\s+/', '', $this->$field);
455
                }
456
            }
457
        }
458
459
        return strtolower(trim($comparisonString));
460
    }
461
462
    /**
463
     * returns the field prefix string for shipping addresses.
464
     *
465
     * @return string
466
     **/
467
    protected function baseClassLinkingToOrder()
468
    {
469
        if (is_a($this, Object::getCustomClass('BillingAddress'))) {
470
            return 'BillingAddress';
471
        } elseif (is_a($this, Object::getCustomClass('ShippingAddress'))) {
472
            return 'ShippingAddress';
473
        }
474
    }
475
476
    /**
477
     * returns the field prefix string for shipping addresses.
478
     *
479
     * @return string
480
     **/
481
    protected function fieldPrefix()
482
    {
483
        if ($this->baseClassLinkingToOrder() == Object::getCustomClass('BillingAddress')) {
484
            return '';
485
        } else {
486
            return 'Shipping';
487
        }
488
    }
489
490
    /**
491
     *@todo: are there times when the Shipping rather than the Billing address should be linked?
492
     * Copies the last address used by the member.
493
     *
494
     * @param object (Member) $member
495
     * @param bool            $write  - should the address be written
496
     *
497
     * @return OrderAddress | ShippingAddress | BillingAddress
498
     **/
499
    public function FillWithLastAddressFromMember(Member $member, $write = false)
500
    {
501
        $excludedFields = array('ID', 'OrderID');
502
        $fieldPrefix = $this->fieldPrefix();
503
        if ($member && $member->exists()) {
504
            $oldAddress = $member->previousOrderAddress($this->baseClassLinkingToOrder(), $this->ID);
505
            if ($oldAddress) {
506
                $fieldNameArray = array_keys($this->Config()->get('db')) + array_keys($this->Config()->get('has_one'));
507
                foreach ($fieldNameArray as $field) {
508
                    if (in_array($field, $excludedFields)) {
509
                        //do nothing
510
                    } elseif ($this->$field) {
511
                        //do nothing
512
                    } elseif (isset($oldAddress->$field)) {
513
                        $this->$field = $oldAddress->$field;
514
                    }
515
                }
516
            }
517
            //copy data from  member
518
            if (is_a($this, Object::getCustomClass('BillingAddress'))) {
519
                $this->Email = $member->Email;
520
            }
521
            $fieldNameArray = array('FirstName' => $fieldPrefix.'FirstName', 'Surname' => $fieldPrefix.'Surname');
522
            foreach ($fieldNameArray as $memberField => $fieldName) {
523
                //NOTE, we always override the Billing Address (which does not have a fieldPrefix)
524
                if (!$this->$fieldName || (is_a($this, Object::getCustomClass('BillingAddress')))) {
525
                    $this->$fieldName = $member->$memberField;
526
                }
527
            }
528
        }
529
        if ($write) {
530
            $this->write();
531
        }
532
533
        return $this;
534
    }
535
536
    /**
537
     * find the member associated with the current Order and address.
538
     *
539
     * @Note: this needs to be public to give DODS (extensions access to this)
540
     * @todo: can wre write $this->Order() instead????
541
     *
542
     * @return DataObject (Member) | Null
543
     **/
544
    public function getMemberFromOrder()
545
    {
546
        if ($this->exists()) {
547
            if ($order = $this->Order()) {
548
                if ($order->exists()) {
549
                    if ($order->MemberID) {
550
                        return Member::get()->byID($order->MemberID);
551
                    }
552
                }
553
            }
554
        }
555
    }
556
557
    /**
558
     * make an address obsolete and include all the addresses that are identical.
559
     *
560
     * @param Member $member
561
     */
562
    public function MakeObsolete(Member $member = null)
563
    {
564
        $addresses = $member->previousOrderAddresses($this->baseClassLinkingToOrder(), $this->ID, $onlyLastRecord = false, $keepDoubles = true);
565
        $comparisonString = $this->comparisonString();
566
        if ($addresses->count()) {
567
            foreach ($addresses as $address) {
568
                if ($address->comparisonString() == $comparisonString) {
569
                    $address->Obsolete = 1;
570
                    $address->write();
571
                }
572
            }
573
        }
574
        $this->Obsolete = 1;
575
        $this->write();
576
    }
577
578
    /**
579
     * standard SS method
580
     * We "hackishly" ensure that the OrderID is set to the right value.
581
     */
582
    public function onAfterWrite()
583
    {
584
        parent::onAfterWrite();
585
        if ($this->exists()) {
586
            $order = Order::get()
587
                ->filter(array($this->ClassName.'ID' => $this->ID))
588
                ->First();
589
            if ($order && $order->ID != $this->OrderID) {
590
                $this->OrderID = $order->ID;
591
                $this->write();
592
            }
593
        }
594
    }
595
596
    /**
597
     * returns the link that can be used to remove (make Obsolete) an address.
598
     *
599
     * @return string
600
     */
601
    public function RemoveLink()
602
    {
603
        return ShoppingCart_Controller::remove_address_link($this->ID, $this->ClassName);
604
    }
605
606
    /**
607
     * converts an address into JSON.
608
     *
609
     * @return string (JSON)
610
     */
611
    public function getJSONData()
612
    {
613
        return $this->JSONData();
614
    }
615
    public function JSONData()
616
    {
617
        $jsArray = array();
618
        if (!isset($fields)) {
619
            $fields = $this->stat('db');
620
            $regionFieldName = $this->fieldPrefix().'RegionID';
621
            $fields[$regionFieldName] = $regionFieldName;
622
        }
623
        if ($fields) {
624
            foreach ($fields as $name => $field) {
625
                $jsArray[$name] = $this->$name;
626
            }
627
        }
628
629
        return Convert::array2json($jsArray);
630
    }
631
632
    /**
633
     * returns the instance of EcommerceDBConfig.
634
     *
635
     * @return EcommerceDBConfig
636
     **/
637
    public function EcomConfig()
638
    {
639
        return EcommerceDBConfig::current_ecommerce_db_config();
640
    }
641
642
    /**
643
     * standard SS Method
644
     * saves the region code.
645
     */
646
    public function onBeforeWrite()
647
    {
648
        parent::onBeforeWrite();
649
        $fieldPrefix = $this->fieldPrefix();
650
        $idField = $fieldPrefix.'RegionID';
651
        if ($this->$idField) {
652
            $region = EcommerceRegion::get()->byID($this->$idField);
653
            if ($region) {
654
                $codeField = $fieldPrefix.'RegionCode';
655
                $this->$codeField = $region->Code;
656
            }
657
        }
658
    }
659
660
    public function debug()
661
    {
662
        return EcommerceTaskDebugCart::debug_object($this);
663
    }
664
}
665