@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 81 lines 2.8 kB view raw
1<?php 2 3/** 4 * Triggers an event every month on the same day of the month, like the 12th 5 * of the month. 6 * 7 * If a given month does not have such a day (for instance, the clock triggers 8 * on the 30th of each month and the month in question is February, which never 9 * has a 30th day), it will trigger on the last day of the month instead. 10 * 11 * Choosing this strategy for subscriptions is predictable (it's easy to 12 * anticipate when a subscription period will end) and fair (billing 13 * periods always have nearly equal length). It also spreads subscriptions 14 * out evenly. If there are issues with billing, this provides an opportunity 15 * for them to be corrected after only a few customers are affected, instead of 16 * (for example) having every subscription fail all at once on the 1st of the 17 * month. 18 */ 19final class PhabricatorSubscriptionTriggerClock 20 extends PhabricatorTriggerClock { 21 22 public function validateProperties(array $properties) { 23 PhutilTypeSpec::checkMap( 24 $properties, 25 array( 26 'start' => 'int', 27 )); 28 } 29 30 public function getNextEventEpoch($last_epoch, $is_reschedule) { 31 $start_epoch = $this->getProperty('start'); 32 if (!$last_epoch) { 33 $last_epoch = $start_epoch; 34 } 35 36 // Constructing DateTime objects like this implies UTC, so we don't need 37 // to set that explicitly. 38 $start = new DateTime('@'.$start_epoch); 39 $last = new DateTime('@'.$last_epoch); 40 41 $year = (int)$last->format('Y'); 42 $month = (int)$last->format('n'); 43 44 // Note that we're getting the day of the month from the start date, not 45 // from the last event date. This lets us schedule on March 31 after moving 46 // the date back to Feb 28. 47 $day = (int)$start->format('j'); 48 49 // We trigger at the same time of day as the original event. Generally, 50 // this means that you should get invoiced at a reasonable local time in 51 // most cases, unless you subscribed at 1AM or something. 52 $hms = $start->format('G:i:s'); 53 54 // Increment the month by 1. 55 $month = $month + 1; 56 57 // If we ran off the end of the calendar, set the month back to January 58 // and increment the year by 1. 59 if ($month > 12) { 60 $month = 1; 61 $year = $year + 1; 62 } 63 64 // Now, move the day backward until it falls in the correct month. If we 65 // pass an invalid date like "2014-2-31", it will internally be parsed 66 // as though we had passed "2014-3-3". 67 while (true) { 68 $next = new DateTime("{$year}-{$month}-{$day} {$hms} UTC"); 69 if ($next->format('n') == $month) { 70 // The month didn't get corrected forward, so we're all set. 71 break; 72 } else { 73 // The month did get corrected forward, so back off a day. 74 $day--; 75 } 76 } 77 78 return (int)$next->format('U'); 79 } 80 81}