Path: blob/master/src/infrastructure/daemon/workers/clock/PhabricatorSubscriptionTriggerClock.php
12242 views
<?php12/**3* Triggers an event every month on the same day of the month, like the 12th4* of the month.5*6* If a given month does not have such a day (for instance, the clock triggers7* on the 30th of each month and the month in question is February, which never8* has a 30th day), it will trigger on the last day of the month instead.9*10* Choosing this strategy for subscriptions is predictable (it's easy to11* anticipate when a subscription period will end) and fair (billing12* periods always have nearly equal length). It also spreads subscriptions13* out evenly. If there are issues with billing, this provides an opportunity14* for them to be corrected after only a few customers are affected, instead of15* (for example) having every subscription fail all at once on the 1st of the16* month.17*/18final class PhabricatorSubscriptionTriggerClock19extends PhabricatorTriggerClock {2021public function validateProperties(array $properties) {22PhutilTypeSpec::checkMap(23$properties,24array(25'start' => 'int',26));27}2829public function getNextEventEpoch($last_epoch, $is_reschedule) {30$start_epoch = $this->getProperty('start');31if (!$last_epoch) {32$last_epoch = $start_epoch;33}3435// Constructing DateTime objects like this implies UTC, so we don't need36// to set that explicitly.37$start = new DateTime('@'.$start_epoch);38$last = new DateTime('@'.$last_epoch);3940$year = (int)$last->format('Y');41$month = (int)$last->format('n');4243// Note that we're getting the day of the month from the start date, not44// from the last event date. This lets us schedule on March 31 after moving45// the date back to Feb 28.46$day = (int)$start->format('j');4748// We trigger at the same time of day as the original event. Generally,49// this means that you should get invoiced at a reasonable local time in50// most cases, unless you subscribed at 1AM or something.51$hms = $start->format('G:i:s');5253// Increment the month by 1.54$month = $month + 1;5556// If we ran off the end of the calendar, set the month back to January57// and increment the year by 1.58if ($month > 12) {59$month = 1;60$year = $year + 1;61}6263// Now, move the day backward until it falls in the correct month. If we64// pass an invalid date like "2014-2-31", it will internally be parsed65// as though we had passed "2014-3-3".66while (true) {67$next = new DateTime("{$year}-{$month}-{$day} {$hms} UTC");68if ($next->format('n') == $month) {69// The month didn't get corrected forward, so we're all set.70break;71} else {72// The month did get corrected forward, so back off a day.73$day--;74}75}7677return (int)$next->format('U');78}7980}818283