Skip to content

Commit d804598

Browse files
author
epriestley
committedJan 30, 2015
Add some of a billing daemon skeleton
Summary: Ref T6881. This adds the worker, and a script to make it easier to test. It doesn't actually invoice anything. I'm intentionally allowing the script to double-bill since it makes testing way easier (by letting you bill the same period over and over again), and provides a tool for recovery if billing screws up. (This diff isn't very interesting, just trying to avoid a 5K-line diff at the end.) Test Plan: Used `bin/phortune invoice ...` to get the worker to print out some date ranges which it would theoretically invoice. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T6881 Differential Revision: https://secure.phabricator.com/D11577
1 parent a65244c commit d804598

10 files changed

+301
-19
lines changed
 

‎bin/phortune

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../scripts/setup/manage_phortune.php

‎scripts/setup/manage_phortune.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
$root = dirname(dirname(dirname(__FILE__)));
5+
require_once $root.'/scripts/__init_script__.php';
6+
7+
$args = new PhutilArgumentParser($argv);
8+
$args->setTagline('manage billing');
9+
$args->setSynopsis(<<<EOSYNOPSIS
10+
**phortune** __command__ [__options__]
11+
Manage billing.
12+
13+
EOSYNOPSIS
14+
);
15+
$args->parseStandardArguments();
16+
17+
$workflows = id(new PhutilSymbolLoader())
18+
->setAncestorClass('PhabricatorPhortuneManagementWorkflow')
19+
->loadObjects();
20+
$workflows[] = new PhutilHelpArgumentWorkflow();
21+
$args->parseWorkflows($workflows);

‎src/__phutil_library_map__.php

+6
Original file line numberDiff line numberDiff line change
@@ -2151,6 +2151,8 @@
21512151
'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php',
21522152
'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
21532153
'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
2154+
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
2155+
'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php',
21542156
'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php',
21552157
'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php',
21562158
'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php',
@@ -2816,6 +2818,7 @@
28162818
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
28172819
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
28182820
'PhortuneSubscriptionViewController' => 'applications/phortune/controller/PhortuneSubscriptionViewController.php',
2821+
'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
28192822
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
28202823
'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
28212824
'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
@@ -5396,6 +5399,8 @@
53965399
'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
53975400
'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
53985401
'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
5402+
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
5403+
'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
53995404
'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
54005405
'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
54015406
'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions',
@@ -6170,6 +6175,7 @@
61706175
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
61716176
'PhortuneSubscriptionTableView' => 'AphrontView',
61726177
'PhortuneSubscriptionViewController' => 'PhortuneController',
6178+
'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
61736179
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
61746180
'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
61756181
'PhragmentBrowseController' => 'PhragmentController',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
final class PhabricatorPhortuneManagementInvoiceWorkflow
4+
extends PhabricatorPhortuneManagementWorkflow {
5+
6+
protected function didConstruct() {
7+
$this
8+
->setName('invoice')
9+
->setSynopsis(
10+
pht(
11+
'Invoices a subscription for a given billing period. This can '.
12+
'charge payment accounts twice.'))
13+
->setArguments(
14+
array(
15+
array(
16+
'name' => 'subscription',
17+
'param' => 'phid',
18+
'help' => pht('Subscription to invoice.'),
19+
),
20+
array(
21+
'name' => 'now',
22+
'param' => 'time',
23+
'help' => pht(
24+
'Bill as though the current time is a specific time.'),
25+
),
26+
array(
27+
'name' => 'last',
28+
'param' => 'time',
29+
'help' => pht('Set the start of the billing period.'),
30+
),
31+
array(
32+
'name' => 'next',
33+
'param' => 'time',
34+
'help' => pht('Set the end of the billing period.'),
35+
),
36+
array(
37+
'name' => 'auto-range',
38+
'help' => pht('Automatically use the current billing period.'),
39+
),
40+
array(
41+
'name' => 'force',
42+
'help' => pht(
43+
'Skip the prompt warning you that this operation is '.
44+
'potentially dangerous.'),
45+
),
46+
));
47+
}
48+
49+
public function execute(PhutilArgumentParser $args) {
50+
$console = PhutilConsole::getConsole();
51+
$viewer = $this->getViewer();
52+
53+
$subscription_phid = $args->getArg('subscription');
54+
if (!$subscription_phid) {
55+
throw new PhutilArgumentUsageException(
56+
pht(
57+
'Specify which subscription to invoice with --subscription.'));
58+
}
59+
60+
$subscription = id(new PhortuneSubscriptionQuery())
61+
->setViewer($viewer)
62+
->withPHIDs(array($subscription_phid))
63+
->needTriggers(true)
64+
->executeOne();
65+
if (!$subscription) {
66+
throw new PhutilArgumentUsageException(
67+
pht(
68+
'Unable to load subscription with PHID "%s".',
69+
$subscription_phid));
70+
}
71+
72+
$now = $args->getArg('now');
73+
$now = $this->parseTimeArgument($now);
74+
if (!$now) {
75+
$now = PhabricatorTime::getNow();
76+
}
77+
78+
$time_guard = PhabricatorTime::pushTime($now, date_default_timezone_get());
79+
80+
$console->writeOut(
81+
"%s\n",
82+
pht(
83+
'Set current time to %s.',
84+
phabricator_datetime(PhabricatorTime::getNow(), $viewer)));
85+
86+
$auto_range = $args->getArg('auto-range');
87+
$last_arg = $args->getArg('last');
88+
$next_arg = $args->getARg('next');
89+
90+
if (!$auto_range && !$last_arg && !$next_arg) {
91+
throw new PhutilArgumentUsageException(
92+
pht(
93+
'Specify a billing range with --last and --next, or use '.
94+
'--auto-range.'));
95+
} else if (!$auto_range & (!$last_arg || !$next_arg)) {
96+
throw new PhutilArgumentUsageException(
97+
pht(
98+
'When specifying --last or --next, you must specify both arguments '.
99+
'to define the beginning and end of the billing range.'));
100+
} else if (!$auto_range && ($last_arg && $next_arg)) {
101+
$last_time = $this->parseTimeArgument($args->getArg('last'));
102+
$next_time = $this->parseTimeArgument($args->getArg('next'));
103+
} else if ($auto_range && ($last_arg || $next_arg)) {
104+
throw new PhutilArgumentUsageException(
105+
pht(
106+
'Use either --auto-range or --last and --next to specify the '.
107+
'billing range, but not both.'));
108+
} else {
109+
$trigger = $subscription->getTrigger();
110+
$event = $trigger->getEvent();
111+
if (!$event) {
112+
throw new PhutilArgumentUsageException(
113+
pht(
114+
'Unable to calculate --auto-range, this subscription has not been '.
115+
'scheduled for billing yet. Wait for the trigger daemon to '.
116+
'schedule the subscription.'));
117+
}
118+
$last_time = $event->getLastEventEpoch();
119+
$next_time = $event->getNextEventEpoch();
120+
}
121+
122+
$console->writeOut(
123+
"%s\n",
124+
pht(
125+
'Preparing to invoice subscription "%s" from %s to %s.',
126+
$subscription->getSubscriptionName(),
127+
($last_time
128+
? phabricator_datetime($last_time, $viewer)
129+
: pht('subscription creation')),
130+
phabricator_datetime($next_time, $viewer)));
131+
132+
PhabricatorWorker::setRunAllTasksInProcess(true);
133+
134+
if (!$args->getArg('force')) {
135+
$console->writeOut(
136+
"**<bg:yellow> %s </bg>**\n%s\n",
137+
pht('WARNING'),
138+
phutil_console_wrap(
139+
pht(
140+
'Manually invoicing will double bill payment accounts if the '.
141+
'range overlaps an existing or future invoice. This script is '.
142+
'intended for testing and development, and should not be part '.
143+
'of routine billing operations. If you continue, you may '.
144+
'incorrectly overcharge customers.')));
145+
146+
if (!phutil_console_confirm(pht('Really invoice this subscription?'))) {
147+
throw new Exception(pht('Declining to invoice.'));
148+
}
149+
}
150+
151+
PhabricatorWorker::scheduleTask(
152+
'PhortuneSubscriptionWorker',
153+
array(
154+
'subscriptionPHID' => $subscription->getPHID(),
155+
'trigger.last-epoch' => $last_time,
156+
'trigger.next-epoch' => $next_time,
157+
),
158+
array(
159+
'objectPHID' => $subscription->getPHID(),
160+
));
161+
162+
return 0;
163+
}
164+
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
3+
abstract class PhabricatorPhortuneManagementWorkflow
4+
extends PhabricatorManagementWorkflow {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
final class PhortuneSubscriptionWorker extends PhabricatorWorker {
4+
5+
protected function doWork() {
6+
$subscription = $this->loadSubscription();
7+
8+
$range = $this->getBillingPeriodRange($subscription);
9+
list($last_epoch, $next_epoch) = $range;
10+
11+
// TODO: Actual billing.
12+
echo "Bill from {$last_epoch} to {$next_epoch}.\n";
13+
}
14+
15+
16+
/**
17+
* Load the subscription to generate an invoice for.
18+
*
19+
* @return PhortuneSubscription The subscription to invoice.
20+
*/
21+
private function loadSubscription() {
22+
$viewer = PhabricatorUser::getOmnipotentUser();
23+
24+
$data = $this->getTaskData();
25+
$subscription_phid = idx($data, 'subscriptionPHID');
26+
27+
$subscription = id(new PhortuneSubscriptionQuery())
28+
->setViewer($viewer)
29+
->withPHIDs(array($subscription_phid))
30+
->executeOne();
31+
if (!$subscription) {
32+
throw new PhabricatorWorkerPermanentFailureException(
33+
pht(
34+
'Failed to load subscription with PHID "%s".',
35+
$subscription_phid));
36+
}
37+
38+
return $subscription;
39+
}
40+
41+
42+
/**
43+
* Get the start and end epoch timestamps for this billing period.
44+
*
45+
* @param PhortuneSubscription The subscription being billed.
46+
* @return pair<int, int> Beginning and end of the billing range.
47+
*/
48+
private function getBillingPeriodRange(PhortuneSubscription $subscription) {
49+
$data = $this->getTaskData();
50+
51+
$last_epoch = idx($data, 'trigger.last-epoch');
52+
if (!$last_epoch) {
53+
// If this is the first time the subscription is firing, use the
54+
// creation date as the start of the billing period.
55+
$last_epoch = $subscription->getDateCreated();
56+
}
57+
$this_epoch = idx($data, 'trigger.next-epoch');
58+
59+
if (!$last_epoch || !$this_epoch) {
60+
throw new PhabricatorWorkerPermanentFailureException(
61+
pht(
62+
'Subscription is missing billing period information.'));
63+
}
64+
65+
$period_length = ($this_epoch - $last_epoch);
66+
if ($period_length <= 0) {
67+
throw new PhabricatorWorkerPermanentFailureException(
68+
pht(
69+
'Subscription has invalid billing period.'));
70+
}
71+
72+
if (PhabricatorTime::getNow() < $this_epoch) {
73+
throw new Exception(
74+
pht(
75+
'Refusing to generate a subscription invoice for a billing period '.
76+
'which ends in the future.'));
77+
}
78+
79+
return array($last_epoch, $this_epoch);
80+
}
81+
82+
}

‎src/infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ public function validateProperties(array $properties) {
3838
public function execute($last_epoch, $this_epoch) {
3939
PhabricatorWorker::scheduleTask(
4040
$this->getProperty('class'),
41-
$this->getProperty('data'),
41+
$this->getProperty('data') + array(
42+
'trigger.last-epoch' => $last_epoch,
43+
'trigger.this-epoch' => $this_epoch,
44+
),
4245
$this->getProperty('options'));
4346
}
4447

‎src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php

+5-5
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,21 @@ public function execute(PhutilArgumentParser $args) {
4747
$triggers = $this->loadTriggers($args);
4848

4949
$now = $args->getArg('now');
50-
$now = $this->parseTime($now);
50+
$now = $this->parseTimeArgument($now);
5151
if (!$now) {
5252
$now = PhabricatorTime::getNow();
5353
}
5454

55-
PhabricatorTime::pushTime($now, date_default_timezone_get());
55+
$time_guard = PhabricatorTime::pushTime($now, date_default_timezone_get());
5656

5757
$console->writeOut(
5858
"%s\n",
5959
pht(
6060
'Set current time to %s.',
6161
phabricator_datetime(PhabricatorTime::getNow(), $viewer)));
6262

63-
$last_time = $this->parseTime($args->getArg('last'));
64-
$next_time = $this->parseTime($args->getArg('next'));
63+
$last_time = $this->parseTimeArgument($args->getArg('last'));
64+
$next_time = $this->parseTimeArgument($args->getArg('next'));
6565

6666
PhabricatorWorker::setRunAllTasksInProcess(true);
6767

@@ -84,7 +84,7 @@ public function execute(PhutilArgumentParser $args) {
8484
$console->writeOut(
8585
"%s\n",
8686
pht(
87-
'Trigger is not scheduled to execute. Use --at to simluate '.
87+
'Trigger is not scheduled to execute. Use --next to simluate '.
8888
'a scheduled event.'));
8989
continue;
9090
} else {

‎src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php

-13
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,4 @@ protected function describeTrigger(PhabricatorWorkerTrigger $trigger) {
4242
return pht('Trigger %d', $trigger->getID());
4343
}
4444

45-
protected function parseTime($time) {
46-
if (!strlen($time)) {
47-
return null;
48-
}
49-
50-
$epoch = strtotime($time);
51-
if ($epoch <= 0) {
52-
throw new PhutilArgumentUsageException(
53-
pht('Unable to parse time "%s".', $time));
54-
}
55-
return $epoch;
56-
}
57-
5845
}

‎src/infrastructure/management/PhabricatorManagementWorkflow.php

+13
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,17 @@ public function getViewer() {
1313
return PhabricatorUser::getOmnipotentUser();
1414
}
1515

16+
protected function parseTimeArgument($time) {
17+
if (!strlen($time)) {
18+
return null;
19+
}
20+
21+
$epoch = strtotime($time);
22+
if ($epoch <= 0) {
23+
throw new PhutilArgumentUsageException(
24+
pht('Unable to parse time "%s".', $time));
25+
}
26+
return $epoch;
27+
}
28+
1629
}

0 commit comments

Comments
 (0)
Failed to load comments.