Commit c6d77f31 authored by jsacksick's avatar jsacksick
Browse files

Issue #3247859 by jsacksick, rinasek, valic: Allow specifying start/end dates for coupons.

parent 2be4f5b1
......@@ -280,3 +280,42 @@ function commerce_promotion_update_8209() {
function commerce_promotion_update_8210() {
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
/**
* Add start & end dates to coupons.
*/
function commerce_promotion_update_8211() {
$fields['start_date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('Start date'))
->setDescription(t('The date the coupon becomes valid.'))
->setRequired(FALSE)
->setSetting('datetime_type', 'datetime')
->setSetting('datetime_optional_label', t('Provide a start date'))
->setDefaultValueCallback('Drupal\commerce_promotion\Entity\Promotion::getDefaultStartDate')
->setDisplayOptions('form', [
'type' => 'commerce_store_datetime',
'weight' => 5,
]);
$fields['end_date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('End date'))
->setDescription(t('The date after which the coupon is invalid.'))
->setRequired(FALSE)
->setSetting('datetime_type', 'datetime')
->setSetting('datetime_optional_label', t('Provide an end date'))
->setDisplayOptions('form', [
'type' => 'commerce_store_datetime',
'weight' => 6,
]);
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
foreach ($fields as $name => $definition) {
// Skip installing the field storage definition if the field already exists.
// This could happen if the commerce_coupon_conditions module is installed
// on the site for example.
if ($definition_update_manager->getFieldStorageDefinition($name, 'commerce_promotion_coupon')) {
continue;
}
$definition_update_manager->installFieldStorageDefinition($name, 'commerce_promotion_coupon', 'commerce_promotion', $definition);
}
}
......@@ -4,10 +4,12 @@ namespace Drupal\commerce_promotion\Entity;
use Drupal\commerce\Entity\CommerceContentEntityBase;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
/**
* Defines the Coupon entity.
......@@ -161,6 +163,42 @@ class Coupon extends CommerceContentEntityBase implements CouponInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function getStartDate($store_timezone = 'UTC') {
if (!$this->get('start_date')->isEmpty()) {
return new DrupalDateTime($this->get('start_date')->value, $store_timezone);
}
}
/**
* {@inheritdoc}
*/
public function setStartDate(DrupalDateTime $start_date) {
$this->get('start_date')->value = $start_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
return $this;
}
/**
* {@inheritdoc}
*/
public function getEndDate($store_timezone = 'UTC') {
if (!$this->get('end_date')->isEmpty()) {
return new DrupalDateTime($this->get('end_date')->value, $store_timezone);
}
}
/**
* {@inheritdoc}
*/
public function setEndDate(DrupalDateTime $end_date = NULL) {
$this->get('end_date')->value = NULL;
if ($end_date) {
$this->get('end_date')->value = $end_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
}
}
/**
* {@inheritdoc}
*/
......@@ -171,6 +209,16 @@ class Coupon extends CommerceContentEntityBase implements CouponInterface {
if (!$this->getPromotion()->available($order)) {
return FALSE;
}
$date = $order->getCalculationDate();
$store_timezone = $date->getTimezone()->getName();
$start_date = $this->getStartDate($store_timezone);
if ($start_date && ($start_date->format('U') > $date->format('U'))) {
return FALSE;
}
$end_date = $this->getEndDate($store_timezone);
if ($end_date && $end_date->format('U') <= $date->format('U')) {
return FALSE;
}
$usage_limit = $this->getUsageLimit();
$usage_limit_customer = $this->getCustomerUsageLimit();
......@@ -268,6 +316,29 @@ class Coupon extends CommerceContentEntityBase implements CouponInterface {
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
$fields['start_date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('Start date'))
->setDescription(t('The date the coupon becomes valid.'))
->setRequired(FALSE)
->setSetting('datetime_type', 'datetime')
->setSetting('datetime_optional_label', t('Provide a start date'))
->setDefaultValueCallback('Drupal\commerce_promotion\Entity\Promotion::getDefaultStartDate')
->setDisplayOptions('form', [
'type' => 'commerce_store_datetime',
'weight' => 5,
]);
$fields['end_date'] = BaseFieldDefinition::create('datetime')
->setLabel(t('End date'))
->setDescription(t('The date after which the coupon is invalid.'))
->setRequired(FALSE)
->setSetting('datetime_type', 'datetime')
->setSetting('datetime_optional_label', t('Provide an end date'))
->setDisplayOptions('form', [
'type' => 'commerce_store_datetime',
'weight' => 6,
]);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Created'))
->setDescription(t('The time when the coupon was created.'));
......
......@@ -3,6 +3,7 @@
namespace Drupal\commerce_promotion\Entity;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
......@@ -123,6 +124,64 @@ interface CouponInterface extends ContentEntityInterface, EntityChangedInterface
*/
public function setEnabled($enabled);
/**
* Gets the coupon start date/time.
*
* The start date/time should always be used in the store timezone.
* Since the promotion can belong to multiple stores, the timezone
* isn't known at load/save time, and is provided by the caller instead.
*
* Note that the returned date/time value is the same in any timezone,
* the "2019-10-17 10:00" stored value is returned as "2019-10-17 10:00 CET"
* for "Europe/Berlin" and "2019-10-17 10:00 ET" for "America/New_York".
*
* @param string $store_timezone
* The store timezone. E.g. "Europe/Berlin".
*
* @return \Drupal\Core\Datetime\DrupalDateTime
* The coupon start date/time.
*/
public function getStartDate($store_timezone = 'UTC');
/**
* Sets the coupon start date/time.
*
* @param \Drupal\Core\Datetime\DrupalDateTime $start_date
* The coupon start date/time.
*
* @return $this
*/
public function setStartDate(DrupalDateTime $start_date);
/**
* Gets the coupon end date/time.
*
* The end date/time should always be used in the store timezone.
* Since the promotion can belong to multiple stores, the timezone
* isn't known at load/save time, and is provided by the caller instead.
*
* Note that the returned date/time value is the same in any timezone,
* the "2019-10-17 11:00" stored value is returned as "2019-10-17 11:00 CET"
* for "Europe/Berlin" and "2019-10-17 11:00 ET" for "America/New_York".
*
* @param string $store_timezone
* The store timezone. E.g. "Europe/Berlin".
*
* @return \Drupal\Core\Datetime\DrupalDateTime
* The coupon end date/time.
*/
public function getEndDate($store_timezone = 'UTC');
/**
* Sets the coupon end date/time.
*
* @param \Drupal\Core\Datetime\DrupalDateTime $end_date
* The coupon end date/time.
*
* @return $this
*/
public function setEndDate(DrupalDateTime $end_date = NULL);
/**
* Checks whether the coupon is available for the given order.
*
......
......@@ -7,6 +7,8 @@ use Drupal\commerce_order\Entity\OrderItem;
use Drupal\commerce_price\Price;
use Drupal\commerce_promotion\Entity\Coupon;
use Drupal\commerce_promotion\Entity\Promotion;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\Tests\commerce_order\Kernel\OrderKernelTestBase;
/**
......@@ -27,6 +29,13 @@ class CouponTest extends OrderKernelTestBase {
'commerce_promotion',
];
/**
* A test order.
*
* @var \Drupal\commerce_order\Entity\OrderInterface
*/
protected $order;
/**
* {@inheritdoc}
*/
......@@ -37,6 +46,27 @@ class CouponTest extends OrderKernelTestBase {
$this->installEntitySchema('commerce_promotion_coupon');
$this->installSchema('commerce_promotion', ['commerce_promotion_usage']);
$this->installConfig(['commerce_promotion']);
$order_item = OrderItem::create([
'type' => 'test',
'quantity' => 1,
'unit_price' => new Price('12.00', 'USD'),
]);
$order = Order::create([
'type' => 'default',
'state' => 'draft',
'mail' => 'test@example.com',
'ip_address' => '127.0.0.1',
'order_number' => '6',
'store_id' => $this->store,
'uid' => $this->createUser(),
'order_items' => [$order_item],
// Used when determining availability, via $order->getCalculationDate().
'placed' => strtotime('2019-11-15 10:14:00'),
]);
$order->setRefreshState(Order::REFRESH_SKIP);
$order->save();
$this->order = $this->reloadEntity($order);
}
/**
......@@ -52,6 +82,10 @@ class CouponTest extends OrderKernelTestBase {
* @covers ::setCustomerUsageLimit
* @covers ::isEnabled
* @covers ::setEnabled
* @covers ::getStartDate
* @covers ::setStartDate
* @covers ::getEndDate
* @covers ::setEndDate
*/
public function testCoupon() {
$promotion = Promotion::create([
......@@ -82,31 +116,25 @@ class CouponTest extends OrderKernelTestBase {
$coupon->setEnabled(TRUE);
$this->assertEquals(TRUE, $coupon->isEnabled());
$date_pattern = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
$time = $this->container->get('datetime.time');
$default_start_date = gmdate($date_pattern, $time->getRequestTime());
$this->assertEquals($default_start_date, $coupon->getStartDate()->format($date_pattern));
$coupon->setStartDate(new DrupalDateTime('2017-01-01 12:12:12'));
$this->assertEquals('2017-01-01 12:12:12 UTC', $coupon->getStartDate()->format('Y-m-d H:i:s T'));
$this->assertEquals('2017-01-01 12:12:12 CET', $coupon->getStartDate('Europe/Berlin')->format('Y-m-d H:i:s T'));
$this->assertNull($coupon->getEndDate());
$coupon->setEndDate(new DrupalDateTime('2017-01-31 17:15:00'));
$this->assertEquals('2017-01-31 17:15:00 UTC', $coupon->getEndDate()->format('Y-m-d H:i:s T'));
$this->assertEquals('2017-01-31 17:15:00 CET', $coupon->getEndDate('Europe/Berlin')->format('Y-m-d H:i:s T'));
}
/**
* @covers ::available
*/
public function testAvailability() {
$order_item = OrderItem::create([
'type' => 'test',
'quantity' => 1,
'unit_price' => new Price('12.00', 'USD'),
]);
$order_item->save();
$order = Order::create([
'type' => 'default',
'state' => 'draft',
'mail' => 'test@example.com',
'ip_address' => '127.0.0.1',
'order_number' => '6',
'store_id' => $this->store,
'uid' => $this->createUser(),
'order_items' => [$order_item],
]);
$order->setRefreshState(Order::REFRESH_SKIP);
$order->save();
$promotion = Promotion::create([
'order_types' => ['default'],
'stores' => [$this->store->id()],
......@@ -123,68 +151,49 @@ class CouponTest extends OrderKernelTestBase {
'code' => 'coupon_code',
'status' => TRUE,
]);
$coupon->setStartDate(DrupalDateTime::createFromTimestamp($this->order->getPlacedTime()));
$coupon->save();
$this->assertTrue($coupon->available($order));
$this->assertTrue($coupon->available($this->order));
$coupon->setEnabled(FALSE);
$this->assertFalse($coupon->available($order));
$this->assertFalse($coupon->available($this->order));
$coupon->setEnabled(TRUE);
$this->container->get('commerce_promotion.usage')->register($order, $promotion, $coupon);
$this->container->get('commerce_promotion.usage')->register($this->order, $promotion, $coupon);
// Test that the promotion usage is checked at the coupon level.
$this->assertFalse($coupon->available($order));
$this->assertFalse($coupon->available($this->order));
$promotion->setUsageLimit(0);
$promotion->setCustomerUsageLimit(0);
$promotion->save();
$promotion = $this->reloadEntity($promotion);
$this->assertTrue($coupon->available($order));
$this->assertTrue($coupon->available($this->order));
// Test the global coupon usage limit.
$coupon->setUsageLimit(1);
$this->assertFalse($coupon->available($order));
$this->assertFalse($coupon->available($this->order));
// Test limit coupon usage by customer.
$coupon->setCustomerUsageLimit(1);
$coupon->setUsageLimit(0);
$coupon->save();
$coupon = $this->reloadEntity($coupon);
$this->assertFalse($coupon->available($order));
$this->assertFalse($coupon->available($this->order));
$order->setEmail('another@example.com');
$order->setRefreshState(Order::REFRESH_SKIP);
$order->save();
$order = $this->reloadEntity($order);
$this->assertTrue($coupon->available($order));
$this->order->setEmail('another@example.com');
$this->order->setRefreshState(Order::REFRESH_SKIP);
$this->order->save();
$this->order = $this->reloadEntity($this->order);
$this->assertTrue($coupon->available($this->order));
\Drupal::service('commerce_promotion.usage')->register($order, $promotion, $coupon);
$this->assertFalse($coupon->available($order));
\Drupal::service('commerce_promotion.usage')->register($this->order, $promotion, $coupon);
$this->assertFalse($coupon->available($this->order));
}
/**
* @covers ::available
*/
public function testAvailabilityAllStores() {
// Test availability for promotions available in all stores.
$order_item = OrderItem::create([
'type' => 'test',
'quantity' => 1,
'unit_price' => new Price('12.00', 'USD'),
]);
$order_item->save();
$order = Order::create([
'type' => 'default',
'state' => 'draft',
'mail' => 'test@example.com',
'ip_address' => '127.0.0.1',
'order_number' => '6',
'store_id' => $this->store,
'uid' => $this->createUser(),
'order_items' => [$order_item],
]);
$order->setRefreshState(Order::REFRESH_SKIP);
$order->save();
$promotion = Promotion::create([
'order_types' => ['default'],
'stores' => [],
......@@ -200,15 +209,88 @@ class CouponTest extends OrderKernelTestBase {
'usage_limit' => 1,
'status' => TRUE,
]);
$coupon->setStartDate(DrupalDateTime::createFromTimestamp($this->order->getPlacedTime()));
$coupon->save();
$this->assertTrue($coupon->available($order));
$this->assertTrue($coupon->available($this->order));
$coupon->setEnabled(FALSE);
$this->assertFalse($coupon->available($order));
$this->assertFalse($coupon->available($this->order));
$coupon->setEnabled(TRUE);
\Drupal::service('commerce_promotion.usage')->register($order, $promotion, $coupon);
$this->assertFalse($coupon->available($order));
\Drupal::service('commerce_promotion.usage')->register($this->order, $promotion, $coupon);
$this->assertFalse($coupon->available($this->order));
}
/**
* Tests the start date logic.
*/
public function testStartDate() {
$promotion = Promotion::create([
'order_types' => ['default'],
'stores' => [],
'usage_limit' => 1,
'start_date' => '2017-01-01',
'status' => TRUE,
]);
$promotion->save();
$coupon = Coupon::create([
'promotion_id' => $promotion->id(),
'code' => 'coupon_code',
'status' => TRUE,
]);
// Start date equal to the order placed date.
$date = new DrupalDateTime('2019-11-15 10:14:00');
$coupon->setStartDate($date);
$this->assertTrue($coupon->available($this->order));
// Past start date.
$date = new DrupalDateTime('2019-11-10 10:14:00');
$coupon->setStartDate($date);
$this->assertTrue($coupon->available($this->order));
// Future start date.
$date = new DrupalDateTime('2019-11-20 10:14:00');
$coupon->setStartDate($date);
$this->assertFalse($coupon->available($this->order));
}
/**
* Tests the end date logic.
*/
public function testEndDate() {
// No end date date.
$promotion = Promotion::create([
'order_types' => ['default'],
'stores' => [$this->store->id()],
'usage_limit' => 1,
'usage_limit_customer' => 0,
'start_date' => '2019-01-01T00:00:00',
'status' => TRUE,
]);
$promotion->save();
$this->assertTrue($promotion->available($this->order));
$coupon = Coupon::create([
'promotion_id' => $promotion->id(),
'code' => 'coupon_code',
'status' => TRUE,
]);
$coupon->setStartDate(DrupalDateTime::createFromTimestamp($this->order->getPlacedTime()));
$this->assertTrue($coupon->available($this->order));
// End date equal to the order placed date.
$date = new DrupalDateTime('2019-11-15 10:14:00');
$coupon->setEndDate($date);
$this->assertFalse($coupon->available($this->order));
// Past end date.
$date = new DrupalDateTime('2017-01-01 00:00:00');
$coupon->setEndDate($date);
$this->assertFalse($coupon->available($this->order));
// Future end date.
$date = new DrupalDateTime('2019-11-20 10:14:00');
$coupon->setEndDate($date);
$this->assertTrue($coupon->available($this->order));
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment