Completed
Push — master ( c5db34...33ec3b )
by Nicolaas
01:34
created

DiscountCouponOption   C

Complexity

Total Complexity 75

Size/Duplication

Total Lines 510
Duplicated Lines 3.92 %

Coupling/Cohesion

Components 3
Dependencies 12

Importance

Changes 0
Metric Value
wmc 75
lcom 3
cbo 12
dl 20
loc 510
rs 5.04
c 0
b 0
f 0

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DiscountCouponOption 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 DiscountCouponOption, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 *@author nicolaas [at] sunnysideup.co.nz
5
 *
6
 **/
7
class DiscountCouponOption extends DataObject
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
8
{
9
    private static $db = array(
10
        'ApplyPercentageToApplicableProducts' => 'Boolean',
11
        'ApplyEvenWithoutCode' => 'Boolean',
12
        'Title' => 'Varchar(255)',
13
        'Code' => 'Varchar(32)',
14
        'NumberOfTimesCouponCanBeUsed' => 'Int',
15
        'StartDate' => 'Date',
16
        'EndDate' => 'Date',
17
        'MaximumDiscount' => 'Currency',
18
        'DiscountAbsolute' => 'Currency',
19
        'DiscountPercentage' => 'Decimal(4,2)',
20
        'MinimumOrderSubTotalValue' => 'Currency'
21
    );
22
23
    private static $many_many = array(
24
        'Products' => 'Product',
25
        'ProductGroups' => 'ProductGroup',
26
        'ProductGroupsMustAlsoBePresentIn' => 'ProductGroup'
27
    );
28
29
    /**
30
     * standard SS variable
31
     *
32
     */
33
    private static $indexes = array(
34
        "Title" => true,
35
        "Code" => true,
36
        "StartDate" => true,
37
        "EndDate" => true
38
    );
39
    /**
40
     * standard SS variable
41
     *
42
     */
43
    private static $casting = array(
44
        "UseCount" => "Int",
45
        "IsValid" => "Boolean",
46
        "IsValidNice" => "Varchar"
47
    );
48
49
50
    /**
51
     * standard SS variable
52
     *
53
     */
54
    private static $searchable_fields = array(
55
        'StartDate' => array(
56
            'filter' => 'DiscountCouponFilterForDate',
57
        ),
58
        'Title' => 'PartialMatchFilter',
59
        "Code" => "PartialMatchFilter",
60
        'ApplyPercentageToApplicableProducts' => 'ExactMatchFilter',
61
        'ApplyEvenWithoutCode' => 'ExactMatchFilter',
62
        'DiscountAbsolute' => 'ExactMatchFilter',
63
        'DiscountPercentage' => 'ExactMatchFilter'
64
    );
65
66
    public function scaffoldSearchFields($_params = null)
67
    {
68
        $fields = parent::scaffoldSearchFields($_params);
69
        $fields->push(
70
            DropdownField::create(
71
                'StartDate',
72
                _t('DiscountCouponOption.FUTURE_CURRENT_OR_PAST', 'Available ...'),
73
                array(
74
                    '' => _t('DiscountCouponOption.ANY_TIME', ' -- Any Time -- '),
75
                    'future' => _t('DiscountCouponOption.FUTURE', 'In Future'),
76
                    'current' => _t('DiscountCouponOption.CURRENT', 'Now'),
77
                    'past' => _t('DiscountCouponOption.PAST', 'No longer available')
78
                )
79
            )
80
        );
81
82
        return $fields;
83
    }
84
85
    /**
86
     * standard SS variable
87
     *
88
     */
89
    private static $field_labels = array(
90
        'StartDate' => 'Start Date',
91
        'EndDate' => 'Last Day',
92
        "Title" => "Name",
93
        "MaximumDiscount" => "Maximum deduction",
94
        "DiscountAbsolute" => "Absolute Discount",
95
        "DiscountPercentage" => "Percentage Discount",
96
        "ApplyPercentageToApplicableProducts" => "Applicable products only",
97
        "NumberOfTimesCouponCanBeUsed" => "Availability count",
98
        "UseCount" => "Count of usage thus far",
99
        "IsValidNice" => "Current validity",
100
        "ApplyEvenWithoutCode" => "Automatically applied",
101
        "Products" => "Applicable products",
102
        "ProductGroups" => "Applicable Categories",
103
        "ProductGroupsMustAlsoBePresentIn" => "Products must also be listed in ... ",
104
    );
105
106
    /**
107
     * standard SS variable
108
     *
109
     */
110
    private static $field_labels_right = array(
111
        "ApplyEvenWithoutCode" => "Discount is automatically applied: the user does not have to enter the coupon at all. ",
112
        "ApplyPercentageToApplicableProducts" => "Rather than applying it to the order, the discount is directly applied to selected products (you must select products).",
113
        "Title" => "The name of the coupon is for internal use only.  This name is not exposed to the customer but can be used to find a particular coupon.",
114
        'Code' => 'The code that the customer enters to get their discount.',
115
        'StartDate' => 'First date the coupon can be used.',
116
        'EndDate' => 'Last day the coupon can be used.',
117
        "MaximumDiscount" => "This is the total amount of discount that can ever be applied - no matter waht. Set to zero to ignore.",
118
        "DiscountAbsolute" => "Absolute reduction. For example, 10 = -$10.00 off. Set this value to zero to ignore.",
119
        "DiscountPercentage" => "Percentage Discount.  For example, 10 = -10% discount Set this value to zero to ignore.",
120
        "MinimumOrderSubTotalValue" => "Minimum sub-total of total order to make coupon applicable. For example, order must be at least $100 before the customer gets a discount.",
121
        "NumberOfTimesCouponCanBeUsed" => "Set to zero to disallow usage, set to 999,999 to allow unlimited usage.",
122
        "UseCount" => "number of times this coupon has been used",
123
        "IsValidNice" => "coupon is currently valid",
124
        "Products" => "This is the final list of products to which the coupon applies. To edit this list directly, please remove all product groups selections in the 'Add Products Using Categories' tab.",
125
        "ProductGroups" => "Adding product categories helps you to select a large number of products at once. Please select categories above.  The products in each category selected will be added to the list.",
126
        "ProductGroupsMustAlsoBePresentIn" => "Select cross-reference listing products (listed in both categories) - e.g. products that are in the Large Items category and Expensive Items category will have a discount.",
127
    );
128
129
    /**
130
     * standard SS variable
131
     *
132
     */
133
    private static $summary_fields = array(
134
        "Title" => "Name",
135
        "Code" => 'Code',
136
        "StartDate.Full" => 'From',
137
        "EndDate.Full" => 'Until',
138
        'IsValidNice' => 'Current'
139
    );
140
141
    /**
142
     * standard SS variable
143
     *
144
     */
145
    private static $defaults = array(
146
        "NumberOfTimesCouponCanBeUsed" => "999999"
147
    );
148
149
    /**
150
     * standard SS variable
151
     *
152
     */
153
    private static $singular_name = "Discount Coupon";
154
    public function i18n_singular_name()
155
    {
156
        return _t("DiscountCouponOption.SINGULAR_NAME", "Discount Coupon");
157
    }
158
159
    /**
160
     * standard SS variable
161
     *
162
     */
163
    private static $plural_name = "Discount Coupons";
164
    public function i18n_plural_name()
165
    {
166
        return _t("DiscountCouponOption.PLURAL_NAME", "Discount Coupons");
167
    }
168
169
    /**
170
     * standard SS variable
171
     *
172
     */
173
    private static $default_sort = [
174
        'EndDate' =>  'DESC',
175
        'StartDate' => 'DESC'
176
        'ID' =>  'ASC'
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_CONSTANT_ENCAPSED_STRING, expecting ']'
Loading history...
177
    ];
178
179
    /**
180
     *
181
     * @var Boolean
182
     */
183
    protected $isNew = false;
184
185
    /**
186
     * standard SS method
187
     *
188
     */
189
    public function populateDefaults()
190
    {
191
        parent::populateDefaults();
192
        $this->Code = $this->createRandomCode();
193
        $this->isNew = true;
194
    }
195
196
    /**
197
     * casted variable
198
     * returns the number of times this coupon has been used.
199
     * Some of the used coupons are not submitted yet, but it should still
200
     * work on first come first served basis.
201
     *
202
     * @return Int
203
     */
204
    public function UseCount()
205
    {
206
        return $this->getUseCount();
207
    }
208
    public function getUseCount()
209
    {
210
        if ($this->ID) {
211
            return DiscountCouponModifier::get()->filter(array("DiscountCouponOptionID" => $this->ID))->count();
212
        }
213
        return 0;
214
    }
215
216
    /**
217
     * casted variable telling us if the discount coupon is valid.
218
     *
219
     * @return Bool
220
     */
221
    public function IsValid()
222
    {
223
        return $this->getIsValid();
224
    }
225
    public function getIsValid()
226
    {
227
        //we go through all the options that would make it invalid...
228
        if (! $this->NumberOfTimesCouponCanBeUsed) {
229
            return false;
230
        }
231
        if ($this->getUseCount() > $this->NumberOfTimesCouponCanBeUsed) {
232
            return false;
233
        }
234
        $now = strtotime("now");
235
        $startDate = strtotime($this->StartDate);
236
        if ($now < $startDate) {
237
            return false;
238
        }
239
        //include the end date itself.
240
        if ($this->EndDate) {
241
            $endDate = strtotime($this->EndDate)+(60*60*24);
242
            if ($now > $endDate) {
243
                return false;
244
            }
245
        }
246
        $additionalChecks = $this->extend('checkForAdditionalValidity');
247
        if (is_array($additionalChecks) && count($additionalChecks)) {
248
            foreach ($additionalChecks as $additionalCheck) {
249
                if (! $additionalCheck) {
250
                    return false;
251
                }
252
            }
253
        }
254
        return true;
255
    }
256
257
    /**
258
     * casted variable telling us if the discount coupon is valid - formatted nicely...
259
     *
260
     * @return String
261
     */
262
    public function IsValidNice()
263
    {
264
        return $this->getIsValidNice();
265
    }
266
    public function getIsValidNice()
267
    {
268
        return $this->IsValid() ? "yes" : "no";
269
    }
270
271
    /**
272
     * standard SS method
273
     * @param Member | NULL
274
     * @return Boolean
275
     */
276
    public function canCreate($member = null)
277
    {
278
        if (Permission::checkMember($member, Config::inst()->get("EcommerceRole", "admin_permission_code"))) {
279
            return true;
280
        }
281
        return parent::canCreate($member);
282
    }
283
284
    /**
285
     * standard SS method
286
     * @param Member | NULL
287
     * @return Boolean
288
     */
289
    public function canView($member = null)
290
    {
291
        if (Permission::checkMember($member, Config::inst()->get("EcommerceRole", "admin_permission_code"))) {
292
            return true;
293
        }
294
        return parent::canView($member);
295
    }
296
297
    /**
298
     * standard SS method
299
     * @param Member | NULL
300
     * @return Boolean
301
     */
302
    public function canEdit($member = null)
303
    {
304
        if (Permission::checkMember($member, Config::inst()->get("EcommerceRole", "admin_permission_code"))) {
305
            return true;
306
        }
307
        return parent::canEdit($member);
308
    }
309
310
    /**
311
     * standard SS method
312
     *
313
     * @param Member | NULL
314
     *
315
     * @return Boolean
316
     */
317
    public function canDelete($member = null)
318
    {
319
        if ($this->UseCount()) {
320
            return false;
321
        }
322
        if (Permission::checkMember($member, Config::inst()->get("EcommerceRole", "admin_permission_code"))) {
323
            return true;
324
        }
325
        return parent::canDelete($member);
326
    }
327
328
    /**
329
     * standard SS method
330
     *
331
     */
332
    public function getCMSFields()
333
    {
334
        $fields = parent::getCMSFields();
335
        $fieldLabels = $this->Config()->get("field_labels_right");
336
        foreach ($fields->dataFields() as $field) {
337
            $name = $field->getName();
338
            if (isset($fieldLabels[$name])) {
339
                $field->setDescription($fieldLabels[$name]);
340
            }
341
        }
342
        if ($this->ApplyEvenWithoutCode) {
343
            $fields->removeFieldsFromTab(
344
                "Root.Main",
345
                array(
346
                    'Code',
347
                    'MaximumDiscount',
348
                    'MinimumOrderSubTotalValue'
349
                )
350
            );
351
        }
352
        $fields->addFieldToTab("Root.Main", new ReadonlyField("UseCount", self::$field_labels["UseCount"]));
353
        $fields->addFieldToTab("Root.Main", new ReadonlyField("IsValidNice", self::$field_labels["IsValidNice"]));
354
        if ($gridField1 = $fields->dataFieldByName("Products")) {
355
            if ($this->ProductGroups()->count() || $this->ProductGroupsMustAlsoBePresentIn()->count()) {
356
                $gridField1->setConfig(GridFieldBasicPageRelationConfigNoAddExisting::create());
357
            } else {
358
                $gridField1->setConfig(GridFieldBasicPageRelationConfig::create());
359
            }
360
            $fields->addFieldToTab("Root.AddProductsDirectly", $gridField1);
361
        }
362
        if ($gridField2 = $fields->dataFieldByName("ProductGroups")) {
363
            $gridField2->setConfig(GridFieldBasicPageRelationConfig::create());
364
            $fields->addFieldToTab("Root.AddProductsUsingCategories", $gridField2);
365
        }
366
367
        if ($gridField3 = $fields->dataFieldByName("ProductGroupsMustAlsoBePresentIn")) {
368
            $gridField3->setConfig(GridFieldBasicPageRelationConfig::create());
369
            $fields->addFieldToTab("Root.AddProductsUsingCategories", $gridField3);
370
        }
371
        $fields->removeFieldFromTab("Root", "Products");
372
        $fields->removeFieldFromTab("Root", "ProductGroups");
373
        $fields->removeFieldFromTab("Root", "ProductGroupsMustAlsoBePresentIn");
374
        if (! $this->ApplyPercentageToApplicableProducts) {
375
            $fields->removeFieldFromTab("Root.Main", "ApplyEvenWithoutCode");
376
        }
377
        return $fields;
378
    }
379
380
    /**
381
     * standard SS method
382
     * THIS ONLY WORKS FOR CREATED OBJECTS
383
     */
384
385
    public function validate()
386
    {
387
        $validator = parent::validate();
388
        if (!$this->isNew) {
389
            if ($this->thereAreCouponsWithTheSameCode()) {
390
                $validator->error(_t('DiscountCouponOption.CODEALREADYEXISTS', "This code already exists - please use another code."));
391
            }
392
            if (isset($_REQUEST["StartDate"])) {
393
                $this->StartDate = date("Y-m-d", strtotime($_REQUEST["StartDate"]));
394
            }
395
            if (isset($_REQUEST["EndDate"])) {
396
                $this->EndDate = date("Y-m-d", strtotime($_REQUEST["EndDate"]));
397
            }
398
            if (strtotime($this->StartDate) < strtotime("-12 years")) {
399
                $validator->error(_t('DiscountCouponOption.NOSTARTDATE', "Please enter a start date"));
400
            }
401
            if (strtotime($this->EndDate) < strtotime("-12 years")) {
402
                $validator->error(_t('DiscountCouponOption.NOENDDATE', "Please enter an end date"));
403
            }
404
            if (strtotime($this->EndDate) < strtotime($this->StartDate)) {
405
                $validator->error(_t('DiscountCouponOption.ENDDATETOOEARLY', "The end date should be after the start date"));
406
            }
407
            if ($this->DiscountPercentage < 0 || $this->DiscountPercentage > 99.999) {
408
                $validator->error(_t('DiscountCouponOption.DISCOUNTOUTOFBOUNDS', "The discount percentage should be between 0 and 99.999."));
409
            }
410
        }
411
        if ($this->NumberOfTimesCouponCanBeUsed === null || $this->NumberOfTimesCouponCanBeUsed === '') {
412
            $validator->error(_t('DiscountCouponOption.SET_TIMES_AVAILABLE', "Set the number of times the coupon is available (0 = not available ... 999,999 = almost unlimited availability)"));
413
        }
414
        return $validator;
415
    }
416
417
    /**
418
     * Checks if there are coupons with the same code as the current one
419
     * @return Boolean
420
     */
421
    protected function thereAreCouponsWithTheSameCode()
422
    {
423
        return DiscountCouponOption::get()->exclude(array("ID" => $this->ID))->filter(array("Code" => $this->Code))->count() ? true : false;
424
    }
425
426
427
    /**
428
     * standard SS method
429
     *
430
     */
431
    public function onBeforeWrite()
432
    {
433
        parent::onBeforeWrite();
434
        if (! $this->Code) {
435
            $this->Code = $this->createRandomCode();
436
        }
437
        $this->Code = preg_replace('/[^a-z0-9]/i', " ", $this->Code);
438
        $this->Code = trim(preg_replace('/\s+/', "", $this->Code));
439
        $i = 1;
440
        while ($this->thereAreCouponsWithTheSameCode() && $i < 100) {
441
            $i++;
442
            $this->Code = $this->Code."_".$i;
443
        }
444
        if (strlen(trim($this->Title)) < 1) {
445
            $this->Title = $this->Code;
446
        }
447
        if ($this->ApplyEvenWithoutCode) {
448
            $this->MaximumDiscount = 0;
449
            $this->MinimumOrderSubTotalValue = 0;
450
        }
451
        if ($this->ApplyPercentageToApplicableProducts) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
452
            //we have removed this!
453
            //$this->DiscountAbsolute = 0;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
454
        } else {
455
            $this->ApplyEvenWithoutCode = 0;
456
        }
457
    }
458
459
    protected $_productsCalculated = false;
460
    /**
461
     * standard SS method
462
     *
463
     */
464
    public function onAfterWrite()
465
    {
466
        $productsArray = array(0 => 0);
467
        $mustAlsoBePresentInProductsArray = array(0 => 0);
468
        parent::onAfterWrite();
469
        if (!$this->_productsCalculated && $this->ProductGroups()->count()) {
470
            $this->_productsCalculated = true;
471
            $productGroups = $this->ProductGroups();
472
            $productsShowable = Product::get()->filter(array("ID" => -1));
473
            foreach ($productGroups as $productGroup) {
474
                $productsShowable = $productGroup->currentInitialProducts(null, "default");
475
                if ($productsShowable && $productsShowable->count()) {
476
                    $productsArray += $productsShowable->map("ID", "ID")->toArray();
477
                }
478
            }
479
            $mustAlsoBePresentInGroups = $this->ProductGroupsMustAlsoBePresentIn();
480
            foreach ($mustAlsoBePresentInGroups as $mustAlsoBePresentInGroup) {
481
                $mustAlsoBePresentInProducts = $mustAlsoBePresentInGroup->currentInitialProducts(null, "default");
482
                if ($mustAlsoBePresentInProducts && $mustAlsoBePresentInProducts->count()) {
483
                    $mustAlsoBePresentInProductsArray += $mustAlsoBePresentInProducts->map("ID", "ID")->toArray();
484
                }
485
            }
486
            if (count($mustAlsoBePresentInProductsArray) > 1) {
487
                $productsArray = array_intersect_key($mustAlsoBePresentInProductsArray, $productsArray);
488
            }
489
            $this->Products()->removeAll();
490
            $this->Products()->addMany($productsArray);
491
            $this->write();
492
        }
493
    }
494
495
    public function onBeforeDelete()
496
    {
497
        parent::onBeforeDelete();
498
        -
499
        DB::query("DELETE FROM \"DiscountCouponOption_Products\" WHERE \"DiscountCouponOptionID\" = ".$this->ID);
500
    }
501
502
    /**
503
     * returns a random string.
504
     * @param Int $length - number of characters
505
     * @param Int $chars - input characters
506
     * @return string
507
     */
508
    protected function createRandomCode($length = 5, $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890')
509
    {
510
        $chars_length = (strlen($chars) - 1);
511
        $string = $chars{rand(0, $chars_length)};
512
        for ($i = 1; $i < $length; $i = strlen($string)) {
513
            $r = $chars{rand(0, $chars_length)};
514
            if ($r != $string{$i - 1}) {
515
                $string .=  $r;
516
            }
517
        }
518
        return $string;
519
    }
520
}
521