Page MenuHomePhabricator

D11577.diff
No OneTemporary

D11577.diff

diff --git a/bin/phortune b/bin/phortune
new file mode 120000
--- /dev/null
+++ b/bin/phortune
@@ -0,0 +1 @@
+../scripts/setup/manage_phortune.php
\ No newline at end of file
diff --git a/scripts/setup/manage_phortune.php b/scripts/setup/manage_phortune.php
new file mode 100755
--- /dev/null
+++ b/scripts/setup/manage_phortune.php
@@ -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);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2151,6 +2151,8 @@
'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 @@
'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 @@
'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
+ 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
+ 'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPhragmentApplication' => 'PhabricatorApplication',
'PhabricatorPhrequentApplication' => 'PhabricatorApplication',
'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions',
@@ -6170,6 +6175,7 @@
'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhortuneSubscriptionTableView' => 'AphrontView',
'PhortuneSubscriptionViewController' => 'PhortuneController',
+ 'PhortuneSubscriptionWorker' => 'PhabricatorWorker',
'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider',
'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider',
'PhragmentBrowseController' => 'PhragmentController',
diff --git a/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php b/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php
@@ -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;
+ }
+
+}
diff --git a/src/applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php b/src/applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorPhortuneManagementWorkflow
+ extends PhabricatorManagementWorkflow {}
diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
@@ -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);
+ }
+
+}
diff --git a/src/infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php b/src/infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php
--- a/src/infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php
+++ b/src/infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php
@@ -38,7 +38,10 @@
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'));
}
diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php
--- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php
+++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php
@@ -47,12 +47,12 @@
$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 @@
'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 @@
$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 {
diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php
--- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php
+++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php
@@ -42,17 +42,4 @@
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;
- }
-
}
diff --git a/src/infrastructure/management/PhabricatorManagementWorkflow.php b/src/infrastructure/management/PhabricatorManagementWorkflow.php
--- a/src/infrastructure/management/PhabricatorManagementWorkflow.php
+++ b/src/infrastructure/management/PhabricatorManagementWorkflow.php
@@ -13,4 +13,17 @@
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;
+ }
+
}

File Metadata

Mime Type
text/plain
Expires
Sun, Dec 29, 4:45 PM (8 h, 48 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6943075
Default Alt Text
D11577.diff (16 KB)

Event Timeline