@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
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}