Promotion.php 28.3 KB
Newer Older
1
2
<?php

3
namespace Drupal\commerce_promotion\Entity;
4

5
use Drupal\commerce\ConditionGroup;
6
use Drupal\commerce\EntityOwnerTrait;
7
use Drupal\commerce\Entity\CommerceContentEntityBase;
8
use Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface;
9
use Drupal\commerce\Plugin\Commerce\Condition\ParentEntityAwareInterface;
10
use Drupal\commerce_order\Entity\OrderInterface;
11
use Drupal\commerce_price\Calculator;
12
use Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\OrderItemPromotionOfferInterface;
13
use Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\PromotionOfferInterface;
14
use Drupal\Core\Datetime\DrupalDateTime;
15
use Drupal\Core\Entity\EntityChangedTrait;
16
use Drupal\Core\Entity\EntityStorageInterface;
17
18
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
19
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
20
21

/**
22
 * Defines the promotion entity class.
23
24
 *
 * @ContentEntityType(
25
 *   id = "commerce_promotion",
26
27
28
29
 *   label = @Translation("Promotion", context = "Commerce"),
 *   label_collection = @Translation("Promotions", context = "Commerce"),
 *   label_singular = @Translation("promotion", context = "Commerce"),
 *   label_plural = @Translation("promotions", context = "Commerce"),
30
 *   label_count = @PluralTranslation(
31
32
 *     singular = "@count promotion",
 *     plural = "@count promotions",
33
 *     context = "Commerce",
34
35
 *   ),
 *   handlers = {
36
37
 *     "event" = "Drupal\commerce_promotion\Event\PromotionEvent",
 *     "storage" = "Drupal\commerce_promotion\PromotionStorage",
38
 *     "access" = "Drupal\entity\EntityAccessControlHandler",
39
 *     "permission_provider" = "Drupal\entity\EntityPermissionProvider",
40
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
41
 *     "list_builder" = "Drupal\commerce_promotion\PromotionListBuilder",
42
 *     "views_data" = "Drupal\commerce_promotion\PromotionViewsData",
43
 *     "form" = {
44
45
 *       "default" = "Drupal\commerce_promotion\Form\PromotionForm",
 *       "add" = "Drupal\commerce_promotion\Form\PromotionForm",
46
47
 *       "enable" = "Drupal\commerce_promotion\Form\PromotionEnableForm",
 *       "disable" = "Drupal\commerce_promotion\Form\PromotionDisableForm",
48
 *       "edit" = "Drupal\commerce_promotion\Form\PromotionForm",
49
 *       "duplicate" = "Drupal\commerce_promotion\Form\PromotionForm",
50
51
 *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm"
 *     },
52
53
54
 *     "local_task_provider" = {
 *       "default" = "Drupal\entity\Menu\DefaultEntityLocalTaskProvider",
 *     },
55
 *     "route_provider" = {
56
 *       "default" = "Drupal\commerce_promotion\PromotionRouteProvider",
57
58
 *       "delete-multiple" = "Drupal\entity\Routing\DeleteMultipleRouteProvider",
 *     },
59
 *     "translation" = "Drupal\commerce_promotion\PromotionTranslationHandler",
60
 *   },
61
62
 *   base_table = "commerce_promotion",
 *   data_table = "commerce_promotion_field_data",
63
 *   admin_permission = "administer commerce_promotion",
64
 *   translatable = TRUE,
65
66
67
68
69
 *   translation = {
 *     "content_translation" = {
 *       "access_callback" = "content_translation_translate_access"
 *     },
 *   },
70
 *   entity_keys = {
71
 *     "id" = "promotion_id",
72
73
74
 *     "label" = "name",
 *     "langcode" = "langcode",
 *     "uuid" = "uuid",
75
 *     "owner" = "uid",
76
77
78
 *     "status" = "status",
 *   },
 *   links = {
79
80
 *     "add-form" = "/promotion/add",
 *     "edit-form" = "/promotion/{commerce_promotion}/edit",
81
82
 *     "enable-form" = "/promotion/{commerce_promotion}/enable",
 *     "disable-form" = "/promotion/{commerce_promotion}/disable",
83
 *     "duplicate-form" = "/promotion/{commerce_promotion}/duplicate",
84
85
86
 *     "delete-form" = "/promotion/{commerce_promotion}/delete",
 *     "delete-multiple-form" = "/admin/commerce/promotions/delete",
 *     "collection" = "/admin/commerce/promotions",
87
 *     "reorder" = "/admin/commerce/promotions/reorder",
88
89
90
91
 *     "drupal:content-translation-overview" = "/promotion/{commerce_promotion}/translations",
 *     "drupal:content-translation-add" = "/promotion/{commerce_promotion}/translations/add/{source}/{target}",
 *     "drupal:content-translation-edit" = "/promotion/{commerce_promotion}/translations/edit/{language}",
 *     "drupal:content-translation-delete" = "/promotion/{commerce_promotion}/translations/delete/{language}",
92
93
94
 *   },
 * )
 */
95
class Promotion extends CommerceContentEntityBase implements PromotionInterface {
96

97
  use EntityChangedTrait;
98
  use EntityOwnerTrait;
99

100
101
102
103
104
105
106
107
108
109
  /**
   * {@inheritdoc}
   */
  public function toUrl($rel = 'canonical', array $options = []) {
    if ($rel == 'canonical') {
      $rel = 'edit-form';
    }
    return parent::toUrl($rel, $options);
  }

110
111
112
113
114
115
116
117
118
119
120
  /**
   * {@inheritdoc}
   */
  public function createDuplicate() {
    $duplicate = parent::createDuplicate();
    // Coupons cannot be transferred because their codes are unique.
    $duplicate->set('coupons', []);

    return $duplicate;
  }

121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
  /**
   * {@inheritdoc}
   */
  public function getName() {
    return $this->get('name')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setName($name) {
    $this->set('name', $name);
    return $this;
  }

136
137
138
  /**
   * {@inheritdoc}
   */
139
140
141
142
143
144
145
146
147
148
149
150
  public function getDisplayName() {
    return $this->get('display_name')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setDisplayName($display_name) {
    $this->set('display_name', $display_name);
    return $this;
  }

151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
  /**
   * {@inheritdoc}
   */
  public function getDescription() {
    return $this->get('description')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setDescription($description) {
    $this->set('description', $description);
    return $this;
  }

166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
  /**
   * {@inheritdoc}
   */
  public function getCreatedTime() {
    return $this->get('created')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setCreatedTime($timestamp) {
    $this->set('created', $timestamp);
    return $this;
  }

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
  /**
   * {@inheritdoc}
   */
  public function getOrderTypes() {
    return $this->get('order_types')->referencedEntities();
  }

  /**
   * {@inheritdoc}
   */
  public function setOrderTypes(array $order_types) {
    $this->set('order_types', $order_types);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getOrderTypeIds() {
    $order_type_ids = [];
    foreach ($this->get('order_types') as $field_item) {
      $order_type_ids[] = $field_item->target_id;
    }
    return $order_type_ids;
  }

  /**
   * {@inheritdoc}
   */
  public function setOrderTypeIds(array $order_type_ids) {
    $this->set('order_types', $order_type_ids);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getStores() {
219
    return $this->getTranslatedReferencedEntities('stores');
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
  }

  /**
   * {@inheritdoc}
   */
  public function setStores(array $stores) {
    $this->set('stores', $stores);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getStoreIds() {
    $store_ids = [];
    foreach ($this->get('stores') as $field_item) {
      $store_ids[] = $field_item->target_id;
    }
    return $store_ids;
  }

  /**
   * {@inheritdoc}
   */
  public function setStoreIds(array $store_ids) {
    $this->set('stores', $store_ids);
    return $this;
  }

bojanz's avatar
bojanz committed
249
250
251
252
253
254
255
256
257
  /**
   * {@inheritdoc}
   */
  public function getOffer() {
    if (!$this->get('offer')->isEmpty()) {
      return $this->get('offer')->first()->getTargetInstance();
    }
  }

258
259
260
261
262
263
264
265
266
267
268
  /**
   * {@inheritdoc}
   */
  public function setOffer(PromotionOfferInterface $offer) {
    $this->set('offer', [
      'target_plugin_id' => $offer->getPluginId(),
      'target_plugin_configuration' => $offer->getConfiguration(),
    ]);
    return $this;
  }

269
270
271
272
273
274
275
  /**
   * {@inheritdoc}
   */
  public function getConditions() {
    $conditions = [];
    foreach ($this->get('conditions') as $field_item) {
      /** @var \Drupal\commerce\Plugin\Field\FieldType\PluginItemInterface $field_item */
276
277
278
279
280
      $condition = $field_item->getTargetInstance();
      if ($condition instanceof ParentEntityAwareInterface) {
        $condition->setParentEntity($this);
      }
      $conditions[] = $condition;
281
282
283
284
    }
    return $conditions;
  }

285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
  /**
   * {@inheritdoc}
   */
  public function setConditions(array $conditions) {
    $this->set('conditions', []);
    foreach ($conditions as $condition) {
      if ($condition instanceof ConditionInterface) {
        $this->get('conditions')->appendItem([
          'target_plugin_id' => $condition->getPluginId(),
          'target_plugin_configuration' => $condition->getConfiguration(),
        ]);
      }
    }
    return $this;
  }

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
  /**
   * {@inheritdoc}
   */
  public function getConditionOperator() {
    return $this->get('condition_operator')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setConditionOperator($condition_operator) {
    $this->set('condition_operator', $condition_operator);
    return $this;
  }

316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
  /**
   * {@inheritdoc}
   */
  public function getCouponIds() {
    $coupon_ids = [];
    foreach ($this->get('coupons') as $field_item) {
      $coupon_ids[] = $field_item->target_id;
    }
    return $coupon_ids;
  }

  /**
   * {@inheritdoc}
   */
  public function getCoupons() {
    $coupons = $this->get('coupons')->referencedEntities();
    return $coupons;
  }

  /**
   * {@inheritdoc}
   */
  public function setCoupons(array $coupons) {
    $this->set('coupons', $coupons);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function hasCoupons() {
    return !$this->get('coupons')->isEmpty();
  }

  /**
   * {@inheritdoc}
   */
  public function addCoupon(CouponInterface $coupon) {
    if (!$this->hasCoupon($coupon)) {
      $this->get('coupons')->appendItem($coupon);
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function removeCoupon(CouponInterface $coupon) {
    $index = $this->getCouponIndex($coupon);
    if ($index !== FALSE) {
      $this->get('coupons')->offsetUnset($index);
    }
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function hasCoupon(CouponInterface $coupon) {
    return in_array($coupon->id(), $this->getCouponIds());
  }

  /**
   * Gets the index of the given coupon.
   *
   * @param \Drupal\commerce_promotion\Entity\CouponInterface $coupon
   *   The coupon.
   *
   * @return int|bool
   *   The index of the given coupon, or FALSE if not found.
   */
  protected function getCouponIndex(CouponInterface $coupon) {
    return array_search($coupon->id(), $this->getCouponIds());
  }

391
392
393
394
  /**
   * {@inheritdoc}
   */
  public function getUsageLimit() {
395
    return $this->get('usage_limit')->value;
396
397
398
399
400
401
402
403
404
405
  }

  /**
   * {@inheritdoc}
   */
  public function setUsageLimit($usage_limit) {
    $this->set('usage_limit', $usage_limit);
    return $this;
  }

406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
  /**
   * {@inheritdoc}
   */
  public function getCustomerUsageLimit() {
    return $this->get('usage_limit_customer')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setCustomerUsageLimit($usage_limit_customer) {
    $this->set('usage_limit_customer', $usage_limit_customer);
    return $this;
  }

421
422
423
  /**
   * {@inheritdoc}
   */
424
425
  public function getStartDate($store_timezone = 'UTC') {
    return new DrupalDateTime($this->get('start_date')->value, $store_timezone);
426
427
428
429
430
431
  }

  /**
   * {@inheritdoc}
   */
  public function setStartDate(DrupalDateTime $start_date) {
432
    $this->get('start_date')->value = $start_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
433
434
435
436
437
438
    return $this;
  }

  /**
   * {@inheritdoc}
   */
439
  public function getEndDate($store_timezone = 'UTC') {
440
    if (!$this->get('end_date')->isEmpty()) {
441
      return new DrupalDateTime($this->get('end_date')->value, $store_timezone);
442
    }
443
444
445
446
447
  }

  /**
   * {@inheritdoc}
   */
448
  public function setEndDate(DrupalDateTime $end_date = NULL) {
449
450
451
452
    $this->get('end_date')->value = NULL;
    if ($end_date) {
      $this->get('end_date')->value = $end_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
    }
453
454
  }

455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
  /**
   * {@inheritdoc}
   */
  public function getCompatibility() {
    return $this->get('compatibility')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setCompatibility($compatibility) {
    if (!in_array($compatibility, [self::COMPATIBLE_NONE, self::COMPATIBLE_ANY])) {
      throw new \InvalidArgumentException('Invalid compatibility type');
    }
    $this->get('compatibility')->value = $compatibility;
    return $this;
  }

473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
  /**
   * {@inheritdoc}
   */
  public function isEnabled() {
    return (bool) $this->getEntityKey('status');
  }

  /**
   * {@inheritdoc}
   */
  public function setEnabled($enabled) {
    $this->set('status', (bool) $enabled);
    return $this;
  }

488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
  /**
   * {@inheritdoc}
   */
  public function getWeight() {
    return (int) $this->get('weight')->value;
  }

  /**
   * {@inheritdoc}
   */
  public function setWeight($weight) {
    $this->set('weight', $weight);
    return $this;
  }

503
504
505
506
507
508
509
  /**
   * {@inheritdoc}
   */
  public function requiresCoupon() {
    return !empty($this->get('require_coupon')->value);
  }

510
511
512
513
514
515
516
  /**
   * {@inheritdoc}
   */
  public function available(OrderInterface $order) {
    if (!$this->isEnabled()) {
      return FALSE;
    }
517
518
519
520
521
    // A promotion that requires a coupon to apply should reference coupons
    // to apply.
    if ($this->requiresCoupon() && !$this->hasCoupons()) {
      return FALSE;
    }
522
523
524
    if (!in_array($order->bundle(), $this->getOrderTypeIds())) {
      return FALSE;
    }
525
    if (!empty($this->getStoreIds()) && !in_array($order->getStoreId(), $this->getStoreIds())) {
526
527
      return FALSE;
    }
528
    $date = $order->getCalculationDate();
529
530
    $store_timezone = $date->getTimezone()->getName();
    $start_date = $this->getStartDate($store_timezone);
531
    if ($start_date->format('U') > $date->format('U')) {
532
533
      return FALSE;
    }
534
    $end_date = $this->getEndDate($store_timezone);
535
    if ($end_date && $end_date->format('U') <= $date->format('U')) {
536
537
      return FALSE;
    }
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556

    $usage_limit = $this->getUsageLimit();
    $usage_limit_customer = $this->getCustomerUsageLimit();
    // If there are no usage limits, the promotion is available.
    if (!$usage_limit && !$usage_limit_customer) {
      return TRUE;
    }
    /** @var \Drupal\commerce_promotion\PromotionUsageInterface $usage */
    $usage = \Drupal::service('commerce_promotion.usage');

    if ($usage_limit && $usage_limit <= $usage->load($this)) {
      return FALSE;
    }
    if ($usage_limit_customer) {
      // Promotion cannot apply to orders without email addresses.
      if (!$email = $order->getEmail()) {
        return FALSE;
      }
      if ($usage_limit_customer <= $usage->load($this, $email)) {
557
558
559
560
561
562
563
        return FALSE;
      }
    }

    return TRUE;
  }

564
565
566
  /**
   * {@inheritdoc}
   */
567
  public function applies(OrderInterface $order) {
568
569
570
571
572
    // Check compatibility.
    // @todo port remaining strategies from Commerce Discount #2762997.
    switch ($this->getCompatibility()) {
      case self::COMPATIBLE_NONE:
        // If there are any existing promotions, then this cannot apply.
573
        foreach ($order->collectAdjustments() as $adjustment) {
574
575
576
577
578
579
580
581
582
583
          if ($adjustment->getType() == 'promotion') {
            return FALSE;
          }
        }
        break;

      case self::COMPATIBLE_ANY:
        break;
    }

584
585
586
    $conditions = $this->getConditions();
    if (!$conditions) {
      // Promotions without conditions always apply.
587
588
      return TRUE;
    }
589
590
591
    // Filter the conditions just in case there are leftover order item
    // conditions (which have been moved to offer conditions).
    $conditions = array_filter($conditions, function ($condition) {
592
593
594
      /** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
      return $condition->getEntityTypeId() == 'commerce_order';
    });
595
    $condition_group = new ConditionGroup($conditions, $this->getConditionOperator());
596

597
    return $condition_group->evaluate($order);
598
599
  }

600
601
602
  /**
   * {@inheritdoc}
   */
603
  public function apply(OrderInterface $order) {
bojanz's avatar
bojanz committed
604
    $offer = $this->getOffer();
605
    if ($offer instanceof OrderItemPromotionOfferInterface) {
606
      $offer_conditions = new ConditionGroup($offer->getConditions(), $offer->getConditionOperator());
bojanz's avatar
bojanz committed
607
608
      // Apply the offer to order items that pass the conditions.
      foreach ($order->getItems() as $order_item) {
609
610
611
612
        // Skip order items with a null unit price or with a quantity = 0.
        if (!$order_item->getUnitPrice() || Calculator::compare($order_item->getQuantity(), '0') === 0) {
          continue;
        }
613
        if ($offer_conditions->evaluate($order_item)) {
bojanz's avatar
bojanz committed
614
615
616
617
          $offer->apply($order_item, $this);
        }
      }
    }
618
619
620
    else {
      $offer->apply($order, $this);
    }
621
622
  }

623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
  /**
   * {@inheritdoc}
   */
  public function clear(OrderInterface $order) {
    $offer = $this->getOffer();
    if ($offer instanceof OrderItemPromotionOfferInterface) {
      foreach ($order->getItems() as $order_item) {
        $offer->clear($order_item, $this);
      }
    }
    else {
      $offer->clear($order, $this);
    }
  }

638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
  /**
   * {@inheritdoc}
   */
  public function preSave(EntityStorageInterface $storage) {
    parent::preSave($storage);

    foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
      $translation = $this->getTranslation($langcode);

      // Explicitly set the owner ID to 0 if the translation owner is anonymous
      // (This will ensure we don't store a broken reference in case the user
      // no longer exists).
      if ($translation->getOwner()->isAnonymous()) {
        $translation->setOwnerId(0);
      }
    }
  }

656
657
658
659
660
661
662
  /**
   * {@inheritdoc}
   */
  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    parent::postSave($storage, $update);

    // Ensure there's a back-reference on each coupon.
663
    foreach ($this->getCoupons() as $coupon) {
664
665
666
667
668
669
670
671
672
673
674
      if (!$coupon->getPromotionId()) {
        $coupon->promotion_id = $this->id();
        $coupon->save();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function postDelete(EntityStorageInterface $storage, array $entities) {
675
    // Delete the linked coupons and usage records.
676
677
678
679
680
681
682
683
684
    $coupons = [];
    foreach ($entities as $entity) {
      foreach ($entity->getCoupons() as $coupon) {
        $coupons[] = $coupon;
      }
    }
    /** @var \Drupal\commerce_promotion\CouponStorageInterface $coupon_storage */
    $coupon_storage = \Drupal::service('entity_type.manager')->getStorage('commerce_promotion_coupon');
    $coupon_storage->delete($coupons);
685
686
    /** @var \Drupal\commerce_promotion\PromotionUsageInterface $usage */
    $usage = \Drupal::service('commerce_promotion.usage');
687
    $usage->delete($entities);
688
689
  }

690
691
692
693
694
  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);
695
    $fields += static::ownerBaseFieldDefinitions($entity_type);
696
697
698

    $fields['name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Name'))
699
      ->setDescription(t('The promotion name.'))
700
701
702
703
704
705
706
707
708
709
710
      ->setRequired(TRUE)
      ->setTranslatable(TRUE)
      ->setSettings([
        'default_value' => '',
        'max_length' => 255,
      ])
      ->setDisplayOptions('form', [
        'type' => 'string_textfield',
        'weight' => 0,
      ])
      ->setDisplayConfigurable('view', TRUE)
711
712
      ->setDisplayConfigurable('form', TRUE);

713
714
715
716
717
718
    $fields['uid']
      ->setLabel(t('Owner'))
      ->setDescription(t('The promotion owner.'))
      ->setDisplayConfigurable('view', TRUE)
      ->setDisplayConfigurable('form', TRUE);

719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
    $fields['display_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Display name'))
      ->setDescription(t('If provided, shown on the order instead of "@translated".', [
        '@translated' => t('Discount'),
      ]))
      ->setTranslatable(TRUE)
      ->setSettings([
        'display_description' => TRUE,
        'default_value' => '',
        'max_length' => 255,
      ])
      ->setDisplayOptions('form', [
        'type' => 'string_textfield',
        'weight' => 0,
      ])
      ->setDisplayConfigurable('view', TRUE)
      ->setDisplayConfigurable('form', TRUE);

737
738
739
740
741
742
743
744
745
746
747
748
749
    $fields['description'] = BaseFieldDefinition::create('string_long')
      ->setLabel(t('Description'))
      ->setDescription(t('Additional information about the promotion to show to the customer'))
      ->setTranslatable(TRUE)
      ->setDefaultValue('')
      ->setDisplayOptions('form', [
        'type' => 'string_textarea',
        'weight' => 1,
        'settings' => [
          'rows' => 3,
        ],
      ])
      ->setDisplayConfigurable('view', TRUE)
750
751
      ->setDisplayConfigurable('form', TRUE);

752
753
754
755
756
757
758
759
760
761
762
    $fields['created'] = BaseFieldDefinition::create('created')
      ->setLabel(t('Created'))
      ->setTranslatable(TRUE)
      ->setDescription(t('The time when the promotion was created.'));

    $fields['changed'] = BaseFieldDefinition::create('changed')
      ->setLabel(t('Changed'))
      ->setTranslatable(TRUE)
      ->setDescription(t('The time when the promotion was last edited.'))
      ->setDisplayConfigurable('view', TRUE);

763
764
    $fields['order_types'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Order types'))
765
      ->setDescription(t('The order types for which the promotion is valid.'))
766
767
768
769
770
771
772
773
774
775
776
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
      ->setRequired(TRUE)
      ->setSetting('target_type', 'commerce_order_type')
      ->setSetting('handler', 'default')
      ->setDisplayOptions('form', [
        'type' => 'commerce_entity_select',
        'weight' => 2,
      ]);

    $fields['stores'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Stores'))
777
      ->setDescription(t('Limit promotion availability to selected stores.'))
778
779
780
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
      ->setSetting('target_type', 'commerce_store')
      ->setSetting('handler', 'default')
781
      ->setSetting('optional_label', t('Restrict to specific stores'))
782
783
784
785
786
      ->setDisplayOptions('form', [
        'type' => 'commerce_entity_select',
        'weight' => 2,
      ]);

787
    $fields['offer'] = BaseFieldDefinition::create('commerce_plugin_item:commerce_promotion_offer')
788
      ->setLabel(t('Offer type'))
789
790
      ->setCardinality(1)
      ->setRequired(TRUE)
791
      ->setSetting('allowed_values_function', [static::class, 'getOfferOptions'])
792
      ->setDisplayOptions('form', [
793
        'type' => 'commerce_plugin_select',
794
795
796
        'weight' => 3,
      ]);

797
    $fields['conditions'] = BaseFieldDefinition::create('commerce_plugin_item:commerce_condition')
798
799
800
801
      ->setLabel(t('Conditions'))
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
      ->setRequired(FALSE)
      ->setDisplayOptions('form', [
802
        'type' => 'commerce_conditions',
803
        'weight' => 3,
804
        'settings' => [
805
          'entity_types' => ['commerce_order'],
806
        ],
807
808
      ]);

809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
    $fields['condition_operator'] = BaseFieldDefinition::create('list_string')
      ->setLabel(t('Condition operator'))
      ->setDescription(t('The condition operator.'))
      ->setRequired(TRUE)
      ->setSetting('allowed_values', [
        'AND' => t('All conditions must pass'),
        'OR' => t('Only one condition must pass'),
      ])
      ->setDisplayOptions('form', [
        'type' => 'options_buttons',
        'weight' => 4,
      ])
      ->setDisplayConfigurable('form', TRUE)
      ->setDefaultValue('AND');

824
825
826
827
    $fields['coupons'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Coupons'))
      ->setDescription(t('Coupons which allow promotion to be redeemed.'))
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
828
      ->setRequired(FALSE)
829
      ->setSetting('target_type', 'commerce_promotion_coupon')
830
      ->setSetting('handler', 'default');
831

832
833
    $fields['usage_limit'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Usage limit'))
834
      ->setDescription(t('The maximum number of times the promotion can be used. 0 for unlimited.'))
835
836
      ->setDefaultValue(0)
      ->setDisplayOptions('form', [
837
        'type' => 'commerce_usage_limit',
838
        'weight' => 4,
839
840
      ]);

841
842
843
844
845
846
847
848
849
    $fields['usage_limit_customer'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Customer usage limit'))
      ->setDescription(t('The maximum number of times the promotion can be used by a customer. 0 for unlimited.'))
      ->setDefaultValue(0)
      ->setDisplayOptions('form', [
        'type' => 'commerce_usage_limit',
        'weight' => 4,
      ]);

850
851
    $fields['start_date'] = BaseFieldDefinition::create('datetime')
      ->setLabel(t('Start date'))
852
      ->setDescription(t('The date the promotion becomes valid.'))
853
      ->setRequired(TRUE)
854
      ->setSetting('datetime_type', 'datetime')
855
      ->setDefaultValueCallback('Drupal\commerce_promotion\Entity\Promotion::getDefaultStartDate')
856
      ->setDisplayOptions('form', [
857
        'type' => 'commerce_store_datetime',
858
        'weight' => 5,
859
860
861
862
      ]);

    $fields['end_date'] = BaseFieldDefinition::create('datetime')
      ->setLabel(t('End date'))
863
      ->setDescription(t('The date after which the promotion is invalid.'))
864
      ->setRequired(FALSE)
865
      ->setSetting('datetime_type', 'datetime')
866
      ->setSetting('datetime_optional_label', t('Provide an end date'))
867
      ->setDisplayOptions('form', [
868
        'type' => 'commerce_store_datetime',
869
        'weight' => 6,
870
871
      ]);

872
873
874
875
876
877
878
879
880
881
    $fields['compatibility'] = BaseFieldDefinition::create('list_string')
      ->setLabel(t('Compatibility with other promotions'))
      ->setSetting('allowed_values_function', ['\Drupal\commerce_promotion\Entity\Promotion', 'getCompatibilityOptions'])
      ->setRequired(TRUE)
      ->setDefaultValue(self::COMPATIBLE_ANY)
      ->setDisplayOptions('form', [
        'type' => 'options_buttons',
        'weight' => 4,
      ]);

882
883
884
885
886
887
888
889
890
891
892
893
    $fields['require_coupon'] = BaseFieldDefinition::create('boolean')
      ->setLabel(t('Require a coupon to apply this promotion'))
      ->setDefaultValue(FALSE)
      ->setDisplayOptions('form', [
        'type' => 'boolean_checkbox',
        'settings' => [
          'display_label' => TRUE,
        ],
      ])
      ->setDisplayConfigurable('view', TRUE)
      ->setDisplayConfigurable('form', TRUE);

894
    $fields['status'] = BaseFieldDefinition::create('boolean')
895
      ->setLabel(t('Status'))
896
      ->setDescription(t('Whether the promotion is enabled.'))
897
      ->setDefaultValue(TRUE)
898
899
      ->setRequired(TRUE)
      ->setSettings([
900
        'on_label' => t('Enabled'),
901
902
        'off_label' => t('Disabled'),
      ])
903
      ->setDisplayOptions('form', [
904
905
        'type' => 'options_buttons',
        'weight' => 0,
906
907
      ]);

908
909
910
    $fields['weight'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Weight'))
      ->setDescription(t('The weight of this promotion in relation to others.'))
911
      ->setDefaultValue(0);
912

913
914
915
916
917
918
919
920
921
922
923
924
    return $fields;
  }

  /**
   * Default value callback for 'start_date' base field definition.
   *
   * @see ::baseFieldDefinitions()
   *
   * @return string
   *   The default value (date string).
   */
  public static function getDefaultStartDate() {
925
    $timestamp = \Drupal::time()->getRequestTime();
926
    return gmdate(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $timestamp);
927
928
  }

929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
  /**
   * Helper callback for uasort() to sort promotions by weight and label.
   *
   * @param \Drupal\commerce_promotion\Entity\PromotionInterface $a
   *   The first promotion to sort.
   * @param \Drupal\commerce_promotion\Entity\PromotionInterface $b
   *   The second promotion to sort.
   *
   * @return int
   *   The comparison result for uasort().
   */
  public static function sort(PromotionInterface $a, PromotionInterface $b) {
    $a_weight = $a->getWeight();
    $b_weight = $b->getWeight();
    if ($a_weight == $b_weight) {
      $a_label = $a->label();
      $b_label = $b->label();
      return strnatcasecmp($a_label, $b_label);
    }
    return ($a_weight < $b_weight) ? -1 : 1;
  }

951
952
953
954
955
956
957
958
959
960
961
962
963
  /**
   * Gets the allowed values for the 'compatibility' base field.
   *
   * @return array
   *   The allowed values.
   */
  public static function getCompatibilityOptions() {
    return [
      self::COMPATIBLE_ANY => t('Any promotion'),
      self::COMPATIBLE_NONE => t('Not with any other promotions'),
    ];
  }

964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
  /**
   * Gets the allowed values for the 'offer' base field.
   *
   * @return array
   *   The allowed values.
   */
  public static function getOfferOptions() {
    /** @var \Drupal\commerce_promotion\PromotionOfferManager $offer_manager */
    $offer_manager = \Drupal::getContainer()->get('plugin.manager.commerce_promotion_offer');
    $plugins = array_map(static function ($definition) {
      return $definition['label'];
    }, $offer_manager->getDefinitions());
    asort($plugins);

    return $plugins;
  }

981
}