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

Kind of generate a bill for users

Summary:
Ref T6881. This generates a product, purchase and invoice for users, and there's sort of some UI for them. Stuff it doesn't do yet:

- Try to autobill when we have a CC;
- actually tell the user they should pay it;
- ask the application for anything like "how much should we charge", or tell the application anything like "the user paid".

However, these work:

- You can //technically// pay the invoices.
- You can see the invoices you paid in the past.

Test Plan: Used `bin/phriction invoice` to double-bill myself over and over again. Paid one of the invoices.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T6881

Differential Revision: https://secure.phabricator.com/D11580

+380 -32
+2
resources/sql/autopatches/20150130.phortune.1.subphid.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phortune.phortune_cart 2 + ADD subscriptionPHID VARBINARY(64);
+2
resources/sql/autopatches/20150130.phortune.2.subkey.sql
··· 1 + ALTER TABLE {$NAMESPACE}_phortune.phortune_cart 2 + ADD KEY `key_subscription` (subscriptionPHID);
+4
src/__phutil_library_map__.php
··· 2811 2811 'PhortuneSchemaSpec' => 'applications/phortune/storage/PhortuneSchemaSpec.php', 2812 2812 'PhortuneStripePaymentProvider' => 'applications/phortune/provider/PhortuneStripePaymentProvider.php', 2813 2813 'PhortuneSubscription' => 'applications/phortune/storage/PhortuneSubscription.php', 2814 + 'PhortuneSubscriptionCart' => 'applications/phortune/cart/PhortuneSubscriptionCart.php', 2814 2815 'PhortuneSubscriptionImplementation' => 'applications/phortune/subscription/PhortuneSubscriptionImplementation.php', 2815 2816 'PhortuneSubscriptionListController' => 'applications/phortune/controller/PhortuneSubscriptionListController.php', 2816 2817 'PhortuneSubscriptionPHIDType' => 'applications/phortune/phid/PhortuneSubscriptionPHIDType.php', 2818 + 'PhortuneSubscriptionProduct' => 'applications/phortune/product/PhortuneSubscriptionProduct.php', 2817 2819 'PhortuneSubscriptionQuery' => 'applications/phortune/query/PhortuneSubscriptionQuery.php', 2818 2820 'PhortuneSubscriptionSearchEngine' => 'applications/phortune/query/PhortuneSubscriptionSearchEngine.php', 2819 2821 'PhortuneSubscriptionTableView' => 'applications/phortune/view/PhortuneSubscriptionTableView.php', ··· 6169 6171 'PhortuneDAO', 6170 6172 'PhabricatorPolicyInterface', 6171 6173 ), 6174 + 'PhortuneSubscriptionCart' => 'PhortuneCartImplementation', 6172 6175 'PhortuneSubscriptionListController' => 'PhortuneController', 6173 6176 'PhortuneSubscriptionPHIDType' => 'PhabricatorPHIDType', 6177 + 'PhortuneSubscriptionProduct' => 'PhortuneProductImplementation', 6174 6178 'PhortuneSubscriptionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 6175 6179 'PhortuneSubscriptionSearchEngine' => 'PhabricatorApplicationSearchEngine', 6176 6180 'PhortuneSubscriptionTableView' => 'AphrontView',
+4
src/applications/phortune/application/PhabricatorPhortuneApplication.php
··· 50 50 => 'PhortuneSubscriptionListController', 51 51 'view/(?P<id>\d+)/' 52 52 => 'PhortuneSubscriptionViewController', 53 + 'order/(?P<subscriptionID>\d+)/' 54 + => 'PhortuneCartListController', 53 55 ), 54 56 'charge/(?:query/(?P<queryKey>[^/]+)/)?' 55 57 => 'PhortuneChargeListController', ··· 90 92 => 'PhortuneSubscriptionListController', 91 93 'view/(?P<id>\d+)/' 92 94 => 'PhortuneSubscriptionViewController', 95 + 'order/(?P<subscriptionID>\d+)/' 96 + => 'PhortuneCartListController', 93 97 ), 94 98 '(?P<id>\d+)/' => 'PhortuneMerchantViewController', 95 99 ),
+89
src/applications/phortune/cart/PhortuneSubscriptionCart.php
··· 1 + <?php 2 + 3 + final class PhortuneSubscriptionCart 4 + extends PhortuneCartImplementation { 5 + 6 + private $subscriptionPHID; 7 + private $subscription; 8 + 9 + public function setSubscriptionPHID($subscription_phid) { 10 + $this->subscriptionPHID = $subscription_phid; 11 + return $this; 12 + } 13 + 14 + public function getSubscriptionPHID() { 15 + return $this->subscriptionPHID; 16 + } 17 + 18 + public function setSubscription(PhortuneSubscription $subscription) { 19 + $this->subscription = $subscription; 20 + return $this; 21 + } 22 + 23 + public function getSubscription() { 24 + return $this->subscription; 25 + } 26 + 27 + public function getName(PhortuneCart $cart) { 28 + return pht('Subscription'); 29 + } 30 + 31 + public function willCreateCart( 32 + PhabricatorUser $viewer, 33 + PhortuneCart $cart) { 34 + 35 + $subscription = $this->getSubscription(); 36 + if (!$subscription) { 37 + throw new Exception( 38 + pht('Call setSubscription() before building a cart!')); 39 + } 40 + 41 + $cart->setMetadataValue('subscriptionPHID', $subscription->getPHID()); 42 + } 43 + 44 + public function loadImplementationsForCarts( 45 + PhabricatorUser $viewer, 46 + array $carts) { 47 + 48 + $phids = array(); 49 + foreach ($carts as $cart) { 50 + $phids[] = $cart->getMetadataValue('subscriptionPHID'); 51 + } 52 + 53 + $subscriptions = id(new PhortuneSubscriptionQuery()) 54 + ->setViewer($viewer) 55 + ->withPHIDs($phids) 56 + ->execute(); 57 + $subscriptions = mpull($subscriptions, null, 'getPHID'); 58 + 59 + $objects = array(); 60 + foreach ($carts as $key => $cart) { 61 + $subscription_phid = $cart->getMetadataValue('subscriptionPHID'); 62 + $subscription = idx($subscriptions, $subscription_phid); 63 + if (!$subscription) { 64 + continue; 65 + } 66 + 67 + $object = id(new PhortuneSubscriptionCart()) 68 + ->setSubscriptionPHID($subscription_phid) 69 + ->setSubscription($subscription); 70 + 71 + $objects[$key] = $object; 72 + } 73 + 74 + return $objects; 75 + } 76 + 77 + public function getCancelURI(PhortuneCart $cart) { 78 + return $this->getSubscription()->getURI(); 79 + } 80 + 81 + public function getDoneURI(PhortuneCart $cart) { 82 + return $this->getSubscription()->getURI(); 83 + } 84 + 85 + public function getDoneActionName(PhortuneCart $cart) { 86 + return pht('Return to Subscription'); 87 + } 88 + 89 + }
+47 -28
src/applications/phortune/controller/PhortuneCartListController.php
··· 3 3 final class PhortuneCartListController 4 4 extends PhortuneController { 5 5 6 - private $accountID; 7 - private $merchantID; 8 - private $queryKey; 9 - 10 6 private $merchant; 11 7 private $account; 8 + private $subscription; 12 9 13 - public function willProcessRequest(array $data) { 14 - $this->merchantID = idx($data, 'merchantID'); 15 - $this->accountID = idx($data, 'accountID'); 16 - $this->queryKey = idx($data, 'queryKey'); 17 - } 10 + public function handleRequest(AphrontRequest $request) { 11 + $viewer = $this->getViewer(); 18 12 19 - public function processRequest() { 20 - $request = $this->getRequest(); 21 - $viewer = $request->getUser(); 13 + $merchant_id = $request->getURIData('merchantID'); 14 + $account_id = $request->getURIData('accountID'); 15 + $subscription_id = $request->getURIData('subscriptionID'); 22 16 23 17 $engine = new PhortuneCartSearchEngine(); 24 18 25 - if ($this->merchantID) { 19 + if ($subscription_id) { 20 + $subscription = id(new PhortuneSubscriptionQuery()) 21 + ->setViewer($viewer) 22 + ->withIDs(array($subscription_id)) 23 + ->executeOne(); 24 + if (!$subscription) { 25 + return new Aphront404Response(); 26 + } 27 + $this->subscription = $subscription; 28 + $engine->setSubscription($subscription); 29 + } 30 + 31 + if ($merchant_id) { 26 32 $merchant = id(new PhortuneMerchantQuery()) 27 33 ->setViewer($viewer) 28 - ->withIDs(array($this->merchantID)) 34 + ->withIDs(array($merchant_id)) 29 35 ->requireCapabilities( 30 36 array( 31 37 PhabricatorPolicyCapability::CAN_VIEW, ··· 37 43 } 38 44 $this->merchant = $merchant; 39 45 $engine->setMerchant($merchant); 40 - } else if ($this->accountID) { 46 + } else if ($account_id) { 41 47 $account = id(new PhortuneAccountQuery()) 42 48 ->setViewer($viewer) 43 - ->withIDs(array($this->accountID)) 49 + ->withIDs(array($account_id)) 44 50 ->requireCapabilities( 45 51 array( 46 52 PhabricatorPolicyCapability::CAN_VIEW, ··· 57 63 } 58 64 59 65 $controller = id(new PhabricatorApplicationSearchController()) 60 - ->setQueryKey($this->queryKey) 66 + ->setQueryKey($request->getURIData('queryKey')) 61 67 ->setSearchEngine($engine) 62 68 ->setNavigation($this->buildSideNavView()); 63 69 ··· 82 88 protected function buildApplicationCrumbs() { 83 89 $crumbs = parent::buildApplicationCrumbs(); 84 90 91 + $subscription = $this->subscription; 92 + 85 93 $merchant = $this->merchant; 86 94 if ($merchant) { 87 95 $id = $merchant->getID(); 88 - $crumbs->addTextCrumb( 89 - $merchant->getName(), 90 - $this->getApplicationURI("merchant/{$id}/")); 91 - $crumbs->addTextCrumb( 92 - pht('Orders'), 93 - $this->getApplicationURI("merchant/orders/{$id}/")); 96 + $this->addMerchantCrumb($crumbs, $merchant); 97 + if (!$subscription) { 98 + $crumbs->addTextCrumb( 99 + pht('Orders'), 100 + $this->getApplicationURI("merchant/orders/{$id}/")); 101 + } 94 102 } 95 103 96 104 $account = $this->account; 97 105 if ($account) { 98 106 $id = $account->getID(); 99 - $crumbs->addTextCrumb( 100 - $account->getName(), 101 - $this->getApplicationURI("{$id}/")); 107 + $this->addAccountCrumb($crumbs, $account); 108 + if (!$subscription) { 109 + $crumbs->addTextCrumb( 110 + pht('Orders'), 111 + $this->getApplicationURI("{$id}/order/")); 112 + } 113 + } 114 + 115 + if ($subscription) { 116 + if ($merchant) { 117 + $subscription_uri = $subscription->getMerchantURI(); 118 + } else { 119 + $subscription_uri = $subscription->getURI(); 120 + } 102 121 $crumbs->addTextCrumb( 103 - pht('Orders'), 104 - $this->getApplicationURI("{$id}/order/")); 122 + $subscription->getSubscriptionName(), 123 + $subscription_uri); 105 124 } 106 125 107 126 return $crumbs;
+60 -2
src/applications/phortune/controller/PhortuneSubscriptionViewController.php
··· 15 15 } 16 16 17 17 $is_merchant = (bool)$request->getURIData('merchantID'); 18 + $merchant = $subscription->getMerchant(); 19 + $account = $subscription->getAccount(); 18 20 19 21 $title = pht('Subscription: %s', $subscription->getSubscriptionName()); 20 22 ··· 27 29 28 30 $crumbs = $this->buildApplicationCrumbs(); 29 31 if ($is_merchant) { 30 - $this->addMerchantCrumb($crumbs, $subscription->getMerchant()); 32 + $this->addMerchantCrumb($crumbs, $merchant); 31 33 } else { 32 - $this->addAccountCrumb($crumbs, $subscription->getAccount()); 34 + $this->addAccountCrumb($crumbs, $account); 33 35 } 34 36 $crumbs->addTextCrumb(pht('Subscription %d', $subscription->getID())); 35 37 ··· 46 48 ->setHeader($header) 47 49 ->addPropertyList($properties); 48 50 51 + $carts = id(new PhortuneCartQuery()) 52 + ->setViewer($viewer) 53 + ->withSubscriptionPHIDs(array($subscription->getPHID())) 54 + ->needPurchases(true) 55 + ->withStatuses( 56 + array( 57 + PhortuneCart::STATUS_PURCHASING, 58 + PhortuneCart::STATUS_CHARGED, 59 + PhortuneCart::STATUS_HOLD, 60 + PhortuneCart::STATUS_REVIEW, 61 + PhortuneCart::STATUS_PURCHASED, 62 + )) 63 + ->execute(); 64 + 65 + $phids = array(); 66 + foreach ($carts as $cart) { 67 + $phids[] = $cart->getPHID(); 68 + foreach ($cart->getPurchases() as $purchase) { 69 + $phids[] = $purchase->getPHID(); 70 + } 71 + } 72 + $handles = $this->loadViewerHandles($phids); 73 + 74 + $invoice_table = id(new PhortuneOrderTableView()) 75 + ->setUser($viewer) 76 + ->setCarts($carts) 77 + ->setHandles($handles); 78 + 79 + $account_id = $account->getID(); 80 + $merchant_id = $merchant->getID(); 81 + $subscription_id = $subscription->getID(); 82 + 83 + if ($is_merchant) { 84 + $invoices_uri = $this->getApplicationURI( 85 + "merchant/{$merchant_id}/subscription/order/{$subscription_id}/"); 86 + } else { 87 + $invoices_uri = $this->getApplicationURI( 88 + "{$account_id}/subscription/order/{$subscription_id}/"); 89 + } 90 + 91 + $invoice_header = id(new PHUIHeaderView()) 92 + ->setHeader(pht('Recent Invoices')) 93 + ->addActionLink( 94 + id(new PHUIButtonView()) 95 + ->setTag('a') 96 + ->setIcon( 97 + id(new PHUIIconView()) 98 + ->setIconFont('fa-list')) 99 + ->setHref($invoices_uri) 100 + ->setText(pht('View All Invoices'))); 101 + 102 + $invoice_box = id(new PHUIObjectBoxView()) 103 + ->setHeader($invoice_header) 104 + ->appendChild($invoice_table); 105 + 49 106 return $this->buildApplicationPage( 50 107 array( 51 108 $crumbs, 52 109 $object_box, 110 + $invoice_box, 53 111 ), 54 112 array( 55 113 'title' => $title,
+90
src/applications/phortune/product/PhortuneSubscriptionProduct.php
··· 1 + <?php 2 + 3 + final class PhortuneSubscriptionProduct 4 + extends PhortuneProductImplementation { 5 + 6 + private $viewer; 7 + private $subscriptionPHID; 8 + private $subscription; 9 + 10 + public function setSubscriptionPHID($subscription_phid) { 11 + $this->subscriptionPHID = $subscription_phid; 12 + return $this; 13 + } 14 + 15 + public function getSubscriptionPHID() { 16 + return $this->subscriptionPHID; 17 + } 18 + 19 + public function setSubscription(PhortuneSubscription $subscription) { 20 + $this->subscription = $subscription; 21 + return $this; 22 + } 23 + 24 + public function getSubscription() { 25 + return $this->subscription; 26 + } 27 + 28 + public function setViewer(PhabricatorUser $viewer) { 29 + $this->viewer = $viewer; 30 + return $this; 31 + } 32 + 33 + public function getViewer() { 34 + return $this->viewer; 35 + } 36 + 37 + public function getRef() { 38 + return $this->getSubscriptionPHID(); 39 + } 40 + 41 + public function getName(PhortuneProduct $product) { 42 + return $this->getSubscription()->getSubscriptionName(); 43 + } 44 + 45 + public function getPriceAsCurrency(PhortuneProduct $product) { 46 + return PhortuneCurrency::newEmptyCurrency(); 47 + } 48 + 49 + public function didPurchaseProduct( 50 + PhortuneProduct $product, 51 + PhortunePurchase $purchase) { 52 + // TODO: Callback the subscription. 53 + return; 54 + } 55 + 56 + public function didRefundProduct( 57 + PhortuneProduct $product, 58 + PhortunePurchase $purchase, 59 + PhortuneCurrency $amount) { 60 + // TODO: Callback the subscription. 61 + return; 62 + } 63 + 64 + public function loadImplementationsForRefs( 65 + PhabricatorUser $viewer, 66 + array $refs) { 67 + 68 + $subscriptions = id(new PhortuneSubscriptionQuery()) 69 + ->setViewer($viewer) 70 + ->withPHIDs($refs) 71 + ->execute(); 72 + $subscriptions = mpull($subscriptions, null, 'getPHID'); 73 + 74 + $objects = array(); 75 + foreach ($refs as $ref) { 76 + $subscription = idx($subscriptions, $ref); 77 + if (!$subscription) { 78 + continue; 79 + } 80 + 81 + $objects[] = id(new PhortuneSubscriptionProduct()) 82 + ->setViewer($viewer) 83 + ->setSubscriptionPHID($ref) 84 + ->setSubscription($subscription); 85 + } 86 + 87 + return $objects; 88 + } 89 + 90 + }
+13
src/applications/phortune/query/PhortuneCartQuery.php
··· 7 7 private $phids; 8 8 private $accountPHIDs; 9 9 private $merchantPHIDs; 10 + private $subscriptionPHIDs; 10 11 private $statuses; 11 12 12 13 private $needPurchases; ··· 28 29 29 30 public function withMerchantPHIDs(array $merchant_phids) { 30 31 $this->merchantPHIDs = $merchant_phids; 32 + return $this; 33 + } 34 + 35 + public function withSubscriptionPHIDs(array $subscription_phids) { 36 + $this->subscriptionPHIDs = $subscription_phids; 31 37 return $this; 32 38 } 33 39 ··· 156 162 $conn, 157 163 'cart.merchantPHID IN (%Ls)', 158 164 $this->merchantPHIDs); 165 + } 166 + 167 + if ($this->subscriptionPHIDs !== null) { 168 + $where[] = qsprintf( 169 + $conn, 170 + 'cart.subscriptionPHID IN (%Ls)', 171 + $this->subscriptionPHIDs); 159 172 } 160 173 161 174 if ($this->statuses !== null) {
+15
src/applications/phortune/query/PhortuneCartSearchEngine.php
··· 5 5 6 6 private $merchant; 7 7 private $account; 8 + private $subscription; 8 9 9 10 public function setAccount(PhortuneAccount $account) { 10 11 $this->account = $account; ··· 22 23 23 24 public function getMerchant() { 24 25 return $this->merchant; 26 + } 27 + 28 + public function setSubscription(PhortuneSubscription $subscription) { 29 + $this->subscription = $subscription; 30 + return $this; 31 + } 32 + 33 + public function getSubscription() { 34 + return $this->subscription; 25 35 } 26 36 27 37 public function getResultTypeDescription() { ··· 81 91 } else { 82 92 throw new Exception(pht('You have no accounts!')); 83 93 } 94 + } 95 + 96 + $subscription = $this->getSubscription(); 97 + if ($subscription) { 98 + $query->withSubscriptionPHIDs(array($subscription->getPHID())); 84 99 } 85 100 86 101 return $query;
+5
src/applications/phortune/storage/PhortuneCart.php
··· 16 16 protected $accountPHID; 17 17 protected $authorPHID; 18 18 protected $merchantPHID; 19 + protected $subscriptionPHID; 19 20 protected $cartClass; 20 21 protected $status; 21 22 protected $metadata = array(); ··· 518 519 'status' => 'text32', 519 520 'cartClass' => 'text128', 520 521 'mailKey' => 'bytes20', 522 + 'subscriptionPHID' => 'phid?', 521 523 ), 522 524 self::CONFIG_KEY_SCHEMA => array( 523 525 'key_account' => array( ··· 525 527 ), 526 528 'key_merchant' => array( 527 529 'columns' => array('merchantPHID'), 530 + ), 531 + 'key_subscription' => array( 532 + 'columns' => array('subscriptionPHID'), 528 533 ), 529 534 ), 530 535 ) + parent::getConfiguration();
+49 -2
src/applications/phortune/worker/PhortuneSubscriptionWorker.php
··· 8 8 $range = $this->getBillingPeriodRange($subscription); 9 9 list($last_epoch, $next_epoch) = $range; 10 10 11 - // TODO: Actual billing. 12 - echo "Bill from {$last_epoch} to {$next_epoch}.\n"; 11 + $account = $subscription->getAccount(); 12 + $merchant = $subscription->getMerchant(); 13 + 14 + $viewer = PhabricatorUser::getOmnipotentUser(); 15 + 16 + $product = id(new PhortuneProductQuery()) 17 + ->setViewer($viewer) 18 + ->withClassAndRef('PhortuneSubscriptionProduct', $subscription->getPHID()) 19 + ->executeOne(); 20 + 21 + $cart_implementation = id(new PhortuneSubscriptionCart()) 22 + ->setSubscription($subscription); 23 + 24 + 25 + // TODO: This isn't really ideal. It would be better to use an application 26 + // actor than the original author of the subscription. In particular, if 27 + // someone initiates a subscription, adds some other account managers, and 28 + // later leaves the company, they'll continue "acting" here indefinitely. 29 + // However, for now, some of the stuff later in the pipeline requires a 30 + // valid actor with a real PHID. The subscription should eventually be 31 + // able to create these invoices "as" the application it is acting on 32 + // behalf of. 33 + $actor = id(new PhabricatorPeopleQuery()) 34 + ->setViewer($viewer) 35 + ->withPHIDs(array($subscription->getAuthorPHID())) 36 + ->executeOne(); 37 + if (!$actor) { 38 + throw new Exception(pht('Failed to load actor to bill subscription!')); 39 + } 40 + 41 + $cart = $account->newCart($actor, $cart_implementation, $merchant); 42 + 43 + $purchase = $cart->newPurchase($actor, $product); 44 + 45 + // TODO: Consider allowing subscriptions to cost an amount other than one 46 + // dollar and twenty-three cents. 47 + $currency = PhortuneCurrency::newFromUserInput($actor, '1.23 USD'); 48 + 49 + $purchase 50 + ->setBasePriceAsCurrency($currency) 51 + ->setMetadataValue('subscriptionPHID', $subscription->getPHID()) 52 + ->save(); 53 + 54 + $cart->setSubscriptionPHID($subscription->getPHID()); 55 + $cart->activateCart(); 56 + 57 + // TODO: Autocharge this, etc.; this is still mostly faked up. 58 + echo 'Okay, made a cart here: '; 59 + echo $cart->getCheckoutURI()."\n\n"; 13 60 } 14 61 15 62