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 +setTagline('manage billing'); +$args->setSynopsis(<<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 @@ +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( + "** %s **\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 @@ +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 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; + } + }