mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-21 22:32:41 +01:00
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
This commit is contained in:
parent
a65244c449
commit
d804598f17
10 changed files with 301 additions and 19 deletions
1
bin/phortune
Symbolic link
1
bin/phortune
Symbolic link
|
@ -0,0 +1 @@
|
|||
../scripts/setup/manage_phortune.php
|
21
scripts/setup/manage_phortune.php
Executable file
21
scripts/setup/manage_phortune.php
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
$root = dirname(dirname(dirname(__FILE__)));
|
||||
require_once $root.'/scripts/__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline('manage billing');
|
||||
$args->setSynopsis(<<<EOSYNOPSIS
|
||||
**phortune** __command__ [__options__]
|
||||
Manage billing.
|
||||
|
||||
EOSYNOPSIS
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
|
||||
$workflows = id(new PhutilSymbolLoader())
|
||||
->setAncestorClass('PhabricatorPhortuneManagementWorkflow')
|
||||
->loadObjects();
|
||||
$workflows[] = new PhutilHelpArgumentWorkflow();
|
||||
$args->parseWorkflows($workflows);
|
|
@ -2151,6 +2151,8 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php',
|
||||
'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
|
||||
'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
|
||||
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
|
||||
'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php',
|
||||
'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php',
|
||||
'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php',
|
||||
'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php',
|
||||
|
@ -2816,6 +2818,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php',
|
||||
'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php',
|
||||
'PhortuneSubscriptionViewController' => 'applications/phortune/controller/PhortuneSubscriptionViewController.php',
|
||||
'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php',
|
||||
'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php',
|
||||
'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php',
|
||||
'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php',
|
||||
|
@ -5396,6 +5399,8 @@ phutil_register_library_map(array(
|
|||
'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
|
||||
'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
|
||||
'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||
'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
|
||||
'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
|
@ -6170,6 +6175,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhortuneSubscriptionTableView' => 'AphrontView',
|
||||
'PhortuneSubscriptionViewController' => 'PhortuneController',
|
||||
'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
|
||||
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
|
||||
'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
|
||||
'PhragmentBrowseController' => 'PhragmentController',
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorPhortuneManagementInvoiceWorkflow
|
||||
extends PhabricatorPhortuneManagementWorkflow {
|
||||
|
||||
protected function didConstruct() {
|
||||
$this
|
||||
->setName('invoice')
|
||||
->setSynopsis(
|
||||
pht(
|
||||
'Invoices a subscription for a given billing period. This can '.
|
||||
'charge payment accounts twice.'))
|
||||
->setArguments(
|
||||
array(
|
||||
array(
|
||||
'name' => 'subscription',
|
||||
'param' => 'phid',
|
||||
'help' => pht('Subscription to invoice.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'now',
|
||||
'param' => 'time',
|
||||
'help' => pht(
|
||||
'Bill as though the current time is a specific time.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'last',
|
||||
'param' => 'time',
|
||||
'help' => pht('Set the start of the billing period.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'next',
|
||||
'param' => 'time',
|
||||
'help' => pht('Set the end of the billing period.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'auto-range',
|
||||
'help' => pht('Automatically use the current billing period.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'force',
|
||||
'help' => pht(
|
||||
'Skip the prompt warning you that this operation is '.
|
||||
'potentially dangerous.'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(PhutilArgumentParser $args) {
|
||||
$console = PhutilConsole::getConsole();
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$subscription_phid = $args->getArg('subscription');
|
||||
if (!$subscription_phid) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Specify which subscription to invoice with --subscription.'));
|
||||
}
|
||||
|
||||
$subscription = id(new PhortuneSubscriptionQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($subscription_phid))
|
||||
->needTriggers(true)
|
||||
->executeOne();
|
||||
if (!$subscription) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Unable to load subscription with PHID "%s".',
|
||||
$subscription_phid));
|
||||
}
|
||||
|
||||
$now = $args->getArg('now');
|
||||
$now = $this->parseTimeArgument($now);
|
||||
if (!$now) {
|
||||
$now = PhabricatorTime::getNow();
|
||||
}
|
||||
|
||||
$time_guard = PhabricatorTime::pushTime($now, date_default_timezone_get());
|
||||
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Set current time to %s.',
|
||||
phabricator_datetime(PhabricatorTime::getNow(), $viewer)));
|
||||
|
||||
$auto_range = $args->getArg('auto-range');
|
||||
$last_arg = $args->getArg('last');
|
||||
$next_arg = $args->getARg('next');
|
||||
|
||||
if (!$auto_range && !$last_arg && !$next_arg) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Specify a billing range with --last and --next, or use '.
|
||||
'--auto-range.'));
|
||||
} else if (!$auto_range & (!$last_arg || !$next_arg)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'When specifying --last or --next, you must specify both arguments '.
|
||||
'to define the beginning and end of the billing range.'));
|
||||
} else if (!$auto_range && ($last_arg && $next_arg)) {
|
||||
$last_time = $this->parseTimeArgument($args->getArg('last'));
|
||||
$next_time = $this->parseTimeArgument($args->getArg('next'));
|
||||
} else if ($auto_range && ($last_arg || $next_arg)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Use either --auto-range or --last and --next to specify the '.
|
||||
'billing range, but not both.'));
|
||||
} else {
|
||||
$trigger = $subscription->getTrigger();
|
||||
$event = $trigger->getEvent();
|
||||
if (!$event) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Unable to calculate --auto-range, this subscription has not been '.
|
||||
'scheduled for billing yet. Wait for the trigger daemon to '.
|
||||
'schedule the subscription.'));
|
||||
}
|
||||
$last_time = $event->getLastEventEpoch();
|
||||
$next_time = $event->getNextEventEpoch();
|
||||
}
|
||||
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Preparing to invoice subscription "%s" from %s to %s.',
|
||||
$subscription->getSubscriptionName(),
|
||||
($last_time
|
||||
? phabricator_datetime($last_time, $viewer)
|
||||
: pht('subscription creation')),
|
||||
phabricator_datetime($next_time, $viewer)));
|
||||
|
||||
PhabricatorWorker::setRunAllTasksInProcess(true);
|
||||
|
||||
if (!$args->getArg('force')) {
|
||||
$console->writeOut(
|
||||
"**<bg:yellow> %s </bg>**\n%s\n",
|
||||
pht('WARNING'),
|
||||
phutil_console_wrap(
|
||||
pht(
|
||||
'Manually invoicing will double bill payment accounts if the '.
|
||||
'range overlaps an existing or future invoice. This script is '.
|
||||
'intended for testing and development, and should not be part '.
|
||||
'of routine billing operations. If you continue, you may '.
|
||||
'incorrectly overcharge customers.')));
|
||||
|
||||
if (!phutil_console_confirm(pht('Really invoice this subscription?'))) {
|
||||
throw new Exception(pht('Declining to invoice.'));
|
||||
}
|
||||
}
|
||||
|
||||
PhabricatorWorker::scheduleTask(
|
||||
'PhortuneSubscriptionWorker',
|
||||
array(
|
||||
'subscriptionPHID' => $subscription->getPHID(),
|
||||
'trigger.last-epoch' => $last_time,
|
||||
'trigger.next-epoch' => $next_time,
|
||||
),
|
||||
array(
|
||||
'objectPHID' => $subscription->getPHID(),
|
||||
));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorPhortuneManagementWorkflow
|
||||
extends PhabricatorManagementWorkflow {}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneSubscriptionWorker extends PhabricatorWorker {
|
||||
|
||||
protected function doWork() {
|
||||
$subscription = $this->loadSubscription();
|
||||
|
||||
$range = $this->getBillingPeriodRange($subscription);
|
||||
list($last_epoch, $next_epoch) = $range;
|
||||
|
||||
// TODO: Actual billing.
|
||||
echo "Bill from {$last_epoch} to {$next_epoch}.\n";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the subscription to generate an invoice for.
|
||||
*
|
||||
* @return PhortuneSubscription The subscription to invoice.
|
||||
*/
|
||||
private function loadSubscription() {
|
||||
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||
|
||||
$data = $this->getTaskData();
|
||||
$subscription_phid = idx($data, 'subscriptionPHID');
|
||||
|
||||
$subscription = id(new PhortuneSubscriptionQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($subscription_phid))
|
||||
->executeOne();
|
||||
if (!$subscription) {
|
||||
throw new PhabricatorWorkerPermanentFailureException(
|
||||
pht(
|
||||
'Failed to load subscription with PHID "%s".',
|
||||
$subscription_phid));
|
||||
}
|
||||
|
||||
return $subscription;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the start and end epoch timestamps for this billing period.
|
||||
*
|
||||
* @param PhortuneSubscription The subscription being billed.
|
||||
* @return pair<int, int> Beginning and end of the billing range.
|
||||
*/
|
||||
private function getBillingPeriodRange(PhortuneSubscription $subscription) {
|
||||
$data = $this->getTaskData();
|
||||
|
||||
$last_epoch = idx($data, 'trigger.last-epoch');
|
||||
if (!$last_epoch) {
|
||||
// If this is the first time the subscription is firing, use the
|
||||
// creation date as the start of the billing period.
|
||||
$last_epoch = $subscription->getDateCreated();
|
||||
}
|
||||
$this_epoch = idx($data, 'trigger.next-epoch');
|
||||
|
||||
if (!$last_epoch || !$this_epoch) {
|
||||
throw new PhabricatorWorkerPermanentFailureException(
|
||||
pht(
|
||||
'Subscription is missing billing period information.'));
|
||||
}
|
||||
|
||||
$period_length = ($this_epoch - $last_epoch);
|
||||
if ($period_length <= 0) {
|
||||
throw new PhabricatorWorkerPermanentFailureException(
|
||||
pht(
|
||||
'Subscription has invalid billing period.'));
|
||||
}
|
||||
|
||||
if (PhabricatorTime::getNow() < $this_epoch) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Refusing to generate a subscription invoice for a billing period '.
|
||||
'which ends in the future.'));
|
||||
}
|
||||
|
||||
return array($last_epoch, $this_epoch);
|
||||
}
|
||||
|
||||
}
|
|
@ -38,7 +38,10 @@ final class PhabricatorScheduleTaskTriggerAction
|
|||
public function execute($last_epoch, $this_epoch) {
|
||||
PhabricatorWorker::scheduleTask(
|
||||
$this->getProperty('class'),
|
||||
$this->getProperty('data'),
|
||||
$this->getProperty('data') + array(
|
||||
'trigger.last-epoch' => $last_epoch,
|
||||
'trigger.this-epoch' => $this_epoch,
|
||||
),
|
||||
$this->getProperty('options'));
|
||||
}
|
||||
|
||||
|
|
|
@ -47,12 +47,12 @@ final class PhabricatorWorkerTriggerManagementFireWorkflow
|
|||
$triggers = $this->loadTriggers($args);
|
||||
|
||||
$now = $args->getArg('now');
|
||||
$now = $this->parseTime($now);
|
||||
$now = $this->parseTimeArgument($now);
|
||||
if (!$now) {
|
||||
$now = PhabricatorTime::getNow();
|
||||
}
|
||||
|
||||
PhabricatorTime::pushTime($now, date_default_timezone_get());
|
||||
$time_guard = PhabricatorTime::pushTime($now, date_default_timezone_get());
|
||||
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
|
@ -60,8 +60,8 @@ final class PhabricatorWorkerTriggerManagementFireWorkflow
|
|||
'Set current time to %s.',
|
||||
phabricator_datetime(PhabricatorTime::getNow(), $viewer)));
|
||||
|
||||
$last_time = $this->parseTime($args->getArg('last'));
|
||||
$next_time = $this->parseTime($args->getArg('next'));
|
||||
$last_time = $this->parseTimeArgument($args->getArg('last'));
|
||||
$next_time = $this->parseTimeArgument($args->getArg('next'));
|
||||
|
||||
PhabricatorWorker::setRunAllTasksInProcess(true);
|
||||
|
||||
|
@ -84,7 +84,7 @@ final class PhabricatorWorkerTriggerManagementFireWorkflow
|
|||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Trigger is not scheduled to execute. Use --at to simluate '.
|
||||
'Trigger is not scheduled to execute. Use --next to simluate '.
|
||||
'a scheduled event.'));
|
||||
continue;
|
||||
} else {
|
||||
|
|
|
@ -42,17 +42,4 @@ abstract class PhabricatorWorkerTriggerManagementWorkflow
|
|||
return pht('Trigger %d', $trigger->getID());
|
||||
}
|
||||
|
||||
protected function parseTime($time) {
|
||||
if (!strlen($time)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$epoch = strtotime($time);
|
||||
if ($epoch <= 0) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Unable to parse time "%s".', $time));
|
||||
}
|
||||
return $epoch;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,4 +13,17 @@ abstract class PhabricatorManagementWorkflow extends PhutilArgumentWorkflow {
|
|||
return PhabricatorUser::getOmnipotentUser();
|
||||
}
|
||||
|
||||
protected function parseTimeArgument($time) {
|
||||
if (!strlen($time)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$epoch = strtotime($time);
|
||||
if ($epoch <= 0) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Unable to parse time "%s".', $time));
|
||||
}
|
||||
return $epoch;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue