Some of our PHP tests at Mover started failing today. In particular they were tests around our account system looking at the start and expiration dates for our plans:
1) Authenticator::->subscriptionStartDate::should be close to now expected 1396389125 to be within 1396302607..1396302727
Some quick calculation shows that the date returned in the tests was 86518 seconds outside of the expected window, coincidentally close to the number of seconds in a day (86400).
It turns out this is a fairly widely discussed complication of the strtotime function in PHP, which we use to calculate the expiration and renewal periods for our plans. Our code to calculate the expiration date for a plan looks something like this:
<?php date_default_timezone_set('UTC'); $now = time(); $expiration = strtotime(date(\DateTime::W3C, $now) . " + 1 month");
Then later when we want to generate the subscription start date we subtract 1 month from the expiration date (why store redundant information in your database?)
$start_time = strtotime(date(\DateTime::W3C, $expiration) . " - 1 month");
This, however, can yield some unexpected results:
$now 2014-03-31T21:58:43+00:00 ($now + 1 month) 2014-05-01T21:58:43+00:00 ($now + 1 month) - 1 month 2014-04-01T21:58:43+00:00
This is because of how PHP’s strtotime() function handles adding one month to a date for which the date does not exist in the next month. For example, if someone subscribed on January 31st, strtotime() will tell me that 1 month after January 31st is March 3rd. Personally this logic surprised me (and apparently many others). Fortunately we are already in the process of porting our account system to Recurly, which handles this problem in a very intuitive way. From Recurly’s FAQ:
Customers are always charged on the closest corresponding date of the following month. For example, a customer who would normally be billed on the 31st of a month will either be billed on the 30th of a 30 day month, or on the 28th of February.