Currently a promotion's availability states:

  • The start time must be GREATER THAN the current time, otherwise it's invalid
  • The current must be LESS THAN OR EQUAL TO the end time

These conditions are always true. Which shows some serious faults in our test coverage, and in the actual test coverage within \Drupal\Tests\commerce_promotion\Kernel\Entity\PromotionTest::testAvailability

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mglaman created an issue. See original summary.

mglaman’s picture

Status: Active » Needs work
FileSize
1.38 KB

Failing test

mglaman’s picture

PR with fix: https://github.com/drupalcommerce/commerce/pull/741

+++ b/modules/promotion/tests/src/Kernel/Entity/PromotionTest.php
@@ -205,10 +205,21 @@ class PromotionTest extends CommerceKernelTestBase {
+    $fake_time->getRequestTime()->willReturn(mktime(0, 0, 0, '02', '15', '2016'));

That meant to me 2017-02-15. And when I change that locally it passes.

I'm really confused. Because when we run #2792653: Checkout coupon pane implementation there's this bug.

mglaman’s picture

FileSize
2.94 KB

Okay, now this fails and makes the issue clear. The promotion which has a start date manually set works fine. However, when a start date is not specified it seems to fail.

mglaman’s picture

So confused.

gmdate, running kernel test where promotion uses generated date time.

Start time: 1495886400
Request time: 1495855424
Difference: 0.35851851851852

changed to just date

Start time: 1495886400
Request time: 1495856189
Difference: 0.34966435185185

So I think the problem is in core?

mglaman’s picture

So confused. I added the following debugging in the entity class for Promotion.

  public static function getDefaultStartDate() {
    $timestamp = \Drupal::time()->getRequestTime();
    print 'Default start time: ' . (new \DateTime())->setTimestamp($timestamp)->format('Y-m-d h:i:s A') . PHP_EOL;
    return date(DATETIME_DATE_STORAGE_FORMAT, $timestamp);
  }
public function available(OrderInterface $order) {
..
    $time = \Drupal::time()->getRequestTime();
    if ($this->getStartDate()->format('U') > $time) {
      print 'Start time: ' . (new \DateTime())->setTimestamp($this->getStartDate()->format('U'))->format('Y-m-d h:i:s A') . PHP_EOL;
      print 'Request time: ' . (new \DateTime())->setTimestamp($time)->format('Y-m-d h:i:s A') . PHP_EOL;
      print 'Difference: ' . ($this->getStartDate()->format('U') - $time) / 3600 . PHP_EOL;
      return FALSE;
    }

The result:

Default start time: 2017-05-27 02:19:04 PM
Start time: 2017-05-27 10:00:00 PM
Request time: 2017-05-27 02:19:04 PM

So, for some reason once the time is passed to TypedData/Field API it jumps. I've had a nearly constant different of almost 8 hours. And actually, at looking at changes, it decreases over time.

mglaman’s picture

more, better testing.

Improved debugging when getting default value

    print 'Default start time: ' . (new \DateTime(gmdate(DATETIME_DATE_STORAGE_FORMAT, $timestamp)))->format('Y-m-d h:i:s A e') . PHP_EOL;
    return gmdate(DATETIME_DATE_STORAGE_FORMAT, $timestamp);

This gets us the following output in test:

Default start time: 2017-05-27 12:00:00 AM Australia/Sydney
Start time: 2017-05-27 10:00:00 PM Australia/Sydney
Request time: 2017-05-27 02:42:39 PM Australia/Sydne

Somewhere core screws our date. The only core test coverage for this TypedData plugin is `\Drupal\KernelTests\Core\TypedData\TypedDataTest::testGetAndSet` and barely is testing.

mglaman’s picture

The issue is the \Drupal\datetime\DateTimeComputed class.

with adjustment

  /**
   * {@inheritdoc}
   */
  public function getStartDate() {
    print 'WTFDRUPAL: ' . (new \DateTime($this->get('start_date')->value))->format('Y-m-d h:i:s A e') . PHP_EOL;

    return $this->get('start_date')->date;
  }

I get this output

Default start time: 2017-05-27 12:00:00 AM Australia/Sydney
WTFDRUPAL: 2017-05-27 12:00:00 AM Australia/Sydney
WTFDRUPAL: 2017-05-27 12:00:00 AM Australia/Sydney
Start time: 2017-05-27 10:00:00 PM Australia/Sydney
Request time: 2017-05-27 02:59:16 PM Australia/Sydney
WTFDRUPAL: 2017-05-27 12:00:00 AM Australia/Sydney

When the value is via DateTimeComputed the timed difference is way off.

mglaman’s picture

It seems the problem is #2632040: [PP-1] Add ability to select a timezone for datetime field.

$date = DrupalDateTime::createFromFormat($storage_format, $value, DATETIME_STORAGE_TIMEZONE);

It defaults timezone to UTC. Working on a fix.

agoradesign’s picture

You surely know these moments, when you struggle with a problem such as you're doing now and already are quite confused and your nerves are frazzled, when suddenly someone comes being nitpicky about some minor important details that don't contribute nothing to solve the real problem? Well, here it is such one :DD

I believe that both the end date comparison as well as the proposed change to the start date comparison are wrong. Instead of ">=" and "<=" you should use ">" and "<". I know, this is only a matter of one second each and theefore relevant for edge cases that probably won't ever happen, and if, no one could prove that it was wrong or right, but we always aim for the correct results ;)

I've just done a pen & paper test. For easier imagining I haven't written down full timestamps, but rather stick to minute granularity in visualization, assuming that every entry is exactly happening at 0 seconds to given hour and minute.

When you have a discount valid from 12:00 to 14:00, then I'd expect that immediately starting from 12:00:00 and not 12:00:01 the promotion is available, as well as 14:00:00 is still valid. The attached pic shows that you should leave the equals sign in both comparisons.

This example leads me to a very interesting question regarding promotions in Commerce 2.x: is it possible to define start and end time granularity on a hour base? I do have an use case: a few weeks ago a customer (running on a different e-commerce platform), was participating on a campaign, which allowed a high discount only during 00:00 and 03:00 in Saturday night. Lacking the possibility to define the promotion end time during the day, they set the alarm clock to manually disable it during the night on a weekend :D

mglaman’s picture

RE: #10. True, we assume midnight.

The test failed last night: https://travis-ci.org/drupalcommerce/commerce/jobs/236614056. Just passed locally. So it's all about the hours.

Based on the linked core issue, I do think we should at least consider the following changes

   public function getStartDate() {
 -    return $this->get('start_date')->date;
 +    // @todo use `date` property once https://www.drupal.org/node/2632040 fixed.
 +    return DrupalDateTime::createFromFormat(DATETIME_DATE_STORAGE_FORMAT, $this->get('start_date')->value);
    }
...
-    return $this->get('end_date')->date;
 +    // @todo use `date` property once https://www.drupal.org/node/2632040 fixed.
 +    if (!$this->get('end_date')->isEmpty()) {
 +      return DrupalDateTime::createFromFormat(DATETIME_DATE_STORAGE_FORMAT, $this->get('end_date')->value);
 +    }

Because the calculated date time does not take into consideration the timezone. Or we should change it. Or not.

Either way, we've had plenty of people having invalid coupons due to start times. Functional tests pass versus kernel tests with our logic.

mglaman’s picture

Title: With current logic, promotions are never valid with their start dates. » Computed field value for DateTime field causes start time validation error
Status: Needs work » Needs review
Related issues: +#2632040: [PP-1] Add ability to select a timezone for datetime field

I found #2632040: [PP-1] Add ability to select a timezone for datetime field. Our errors were using the computed date field value and not the raw timestamp and generating our own date time object. This bug only realized itself around evening time.

The PR here has tests and now tests this bug specifically, and has the fix: https://github.com/drupalcommerce/commerce/pull/741

mglaman’s picture

Patch for those who need it

  • bojanz committed 1fb7bc6 on 8.x-2.x authored by mglaman
    Issue #2881791 by mglaman, agoradesign: Computed field value for...
bojanz’s picture

Status: Needs review » Fixed

Fixing this became a real late night adventure because a test fix showed that DrupalDateTime::createFromFormat() actually defaults the time to the current time, instead of 00:00:00. Fixed by not using it.

bojanz’s picture

Also, note that there's another problem here: because the date field stores no timezone, the promotion start/end dates are always specific to the customer's timezone. If you specified June 1st - June 10th assuming the EST timezone, and I'm in the GMT timezone, then the promotion will start earlier for me than for someone who's in EST. We could solve this by having a $store->timezone that's used for these things.

This example leads me to a very interesting question regarding promotions in Commerce 2.x: is it possible to define start and end time granularity on a hour base?

Should be possible to do a hook_entity_base_fields_alter() and change the datetime_type for the fields from date to datetime. The widgets respect that. A more interesting question is whether hours should be allowed by default. That's something that we can change at any time, since the field schema doesn't depend on it.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

mengi’s picture

Would be great to have a timezone for the store and be able to control the time for promotions to start/end. I think this should be the default with midnight entered.