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

Configure Feed

Select the types of activity you want to include in your feed.

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

+301 -19
+1
bin/phortune
··· 1 + ../scripts/setup/manage_phortune.php
+21
scripts/setup/manage_phortune.php
··· 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);
+6
src/__phutil_library_map__.php
··· 2151 2151 'PhabricatorPholioConfigOptions' => 'applications/pholio/config/PhabricatorPholioConfigOptions.php', 2152 2152 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', 2153 2153 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', 2154 + 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php', 2155 + 'PhabricatorPhortuneManagementWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php', 2154 2156 'PhabricatorPhragmentApplication' => 'applications/phragment/application/PhabricatorPhragmentApplication.php', 2155 2157 'PhabricatorPhrequentApplication' => 'applications/phrequent/application/PhabricatorPhrequentApplication.php', 2156 2158 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', ··· 2816 2818 'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php', 2817 2819 'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php', 2818 2820 'PhortuneSubscriptionViewController' => 'applications/phortune/controller/PhortuneSubscriptionViewController.php', 2821 + 'PhortuneSubscriptionWorker' => 'applications/phortune/worker/PhortuneSubscriptionWorker.php', 2819 2822 'PhortuneTestPaymentProvider' => 'applications/phortune/provider/PhortuneTestPaymentProvider.php', 2820 2823 'PhortuneWePayPaymentProvider' => 'applications/phortune/provider/PhortuneWePayPaymentProvider.php', 2821 2824 'PhragmentBrowseController' => 'applications/phragment/controller/PhragmentBrowseController.php', ··· 5396 5399 'PhabricatorPholioConfigOptions' => 'PhabricatorApplicationConfigOptions', 5397 5400 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 5398 5401 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 5402 + 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow', 5403 + 'PhabricatorPhortuneManagementWorkflow' => 'PhabricatorManagementWorkflow', 5399 5404 'PhabricatorPhragmentApplication' => 'PhabricatorApplication', 5400 5405 'PhabricatorPhrequentApplication' => 'PhabricatorApplication', 5401 5406 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', ··· 6170 6175 'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine', 6171 6176 'PhortuneSubscriptionTableView' => 'AphrontView', 6172 6177 'PhortuneSubscriptionViewController' => 'PhortuneController', 6178 + 'PhortuneSubscriptionWorker' => 'PhabricatorWorker', 6173 6179 'PhortuneTestPaymentProvider' => 'PhortunePaymentProvider', 6174 6180 'PhortuneWePayPaymentProvider' => 'PhortunePaymentProvider', 6175 6181 'PhragmentBrowseController' => 'PhragmentController',
+165
src/applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php
··· 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 + }
+4
src/applications/phortune/management/PhabricatorPhortuneManagementWorkflow.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorPhortuneManagementWorkflow 4 + extends PhabricatorManagementWorkflow {}
+82
src/applications/phortune/worker/PhortuneSubscriptionWorker.php
··· 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 + }
+4 -1
src/infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php
··· 38 38 public function execute($last_epoch, $this_epoch) { 39 39 PhabricatorWorker::scheduleTask( 40 40 $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 + ), 42 45 $this->getProperty('options')); 43 46 } 44 47
+5 -5
src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementFireWorkflow.php
··· 47 47 $triggers = $this->loadTriggers($args); 48 48 49 49 $now = $args->getArg('now'); 50 - $now = $this->parseTime($now); 50 + $now = $this->parseTimeArgument($now); 51 51 if (!$now) { 52 52 $now = PhabricatorTime::getNow(); 53 53 } 54 54 55 - PhabricatorTime::pushTime($now, date_default_timezone_get()); 55 + $time_guard = PhabricatorTime::pushTime($now, date_default_timezone_get()); 56 56 57 57 $console->writeOut( 58 58 "%s\n", ··· 60 60 'Set current time to %s.', 61 61 phabricator_datetime(PhabricatorTime::getNow(), $viewer))); 62 62 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')); 65 65 66 66 PhabricatorWorker::setRunAllTasksInProcess(true); 67 67 ··· 84 84 $console->writeOut( 85 85 "%s\n", 86 86 pht( 87 - 'Trigger is not scheduled to execute. Use --at to simluate '. 87 + 'Trigger is not scheduled to execute. Use --next to simluate '. 88 88 'a scheduled event.')); 89 89 continue; 90 90 } else {
-13
src/infrastructure/daemon/workers/management/PhabricatorWorkerTriggerManagementWorkflow.php
··· 42 42 return pht('Trigger %d', $trigger->getID()); 43 43 } 44 44 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 - 58 45 }
+13
src/infrastructure/management/PhabricatorManagementWorkflow.php
··· 13 13 return PhabricatorUser::getOmnipotentUser(); 14 14 } 15 15 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 + 16 29 }