diff --git a/resources/sql/autopatches/20141007.fundmerchant.sql b/resources/sql/autopatches/20141007.fundmerchant.sql new file mode 100644 index 0000000000..f75afec46f --- /dev/null +++ b/resources/sql/autopatches/20141007.fundmerchant.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_fund.fund_initiative + ADD merchantPHID VARBINARY(64); diff --git a/resources/sql/autopatches/20141007.phortunecartmerchant.sql b/resources/sql/autopatches/20141007.phortunecartmerchant.sql new file mode 100644 index 0000000000..930c31b854 --- /dev/null +++ b/resources/sql/autopatches/20141007.phortunecartmerchant.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_phortune.phortune_cart + ADD merchantPHID VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_cart + ADD KEY `key_merchant` (merchantPHID); diff --git a/resources/sql/autopatches/20141007.phortunecharge.sql b/resources/sql/autopatches/20141007.phortunecharge.sql new file mode 100644 index 0000000000..a146adc196 --- /dev/null +++ b/resources/sql/autopatches/20141007.phortunecharge.sql @@ -0,0 +1,16 @@ +TRUNCATE TABLE {$NAMESPACE}_phortune.phortune_charge; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_charge + DROP paymentProviderKey; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_charge + ADD merchantPHID VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_charge + ADD providerPHID VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_charge + ADD KEY `key_merchant` (merchantPHID); + +ALTER TABLE {$NAMESPACE}_phortune.phortune_charge + ADD KEY `key_provider` (providerPHID); diff --git a/resources/sql/autopatches/20141007.phortunepayment.sql b/resources/sql/autopatches/20141007.phortunepayment.sql new file mode 100644 index 0000000000..6afb830589 --- /dev/null +++ b/resources/sql/autopatches/20141007.phortunepayment.sql @@ -0,0 +1,16 @@ +TRUNCATE TABLE {$NAMESPACE}_phortune.phortune_paymentmethod; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod + DROP providerType; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod + DROP providerDomain; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod + ADD merchantPHID VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod + ADD providerPHID VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_phortune.phortune_paymentmethod + ADD KEY `key_merchant` (merchantPHID, accountPHID); diff --git a/src/applications/fund/controller/FundInitiativeBackController.php b/src/applications/fund/controller/FundInitiativeBackController.php index b97786c2b3..a5adb17b7b 100644 --- a/src/applications/fund/controller/FundInitiativeBackController.php +++ b/src/applications/fund/controller/FundInitiativeBackController.php @@ -1,120 +1,128 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $initiative = id(new FundInitiativeQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$initiative) { return new Aphront404Response(); } + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withPHIDs(array($initiative->getMerchantPHID())) + ->executeOne(); + if (!$merchant) { + return new Aphront404Response(); + } + $initiative_uri = '/'.$initiative->getMonogram(); if ($initiative->isClosed()) { return $this->newDialog() ->setTitle(pht('Initiative Closed')) ->appendParagraph( pht('You can not back a closed initiative.')) ->addCancelButton($initiative_uri); } $v_amount = null; $e_amount = true; $errors = array(); if ($request->isFormPost()) { $v_amount = $request->getStr('amount'); if (!strlen($v_amount)) { $errors[] = pht( 'You must specify how much money you want to contribute to the '. 'initiative.'); $e_amount = pht('Required'); } else { try { $currency = PhortuneCurrency::newFromUserInput( $viewer, $v_amount); $currency->assertInRange('1.00 USD', null); } catch (Exception $ex) { $errors[] = $ex->getMessage(); $e_amount = pht('Invalid'); } } if (!$errors) { $backer = FundBacker::initializeNewBacker($viewer) ->setInitiativePHID($initiative->getPHID()) ->attachInitiative($initiative) ->setAmountAsCurrency($currency) ->save(); $product = id(new PhortuneProductQuery()) ->setViewer($viewer) ->withClassAndRef('FundBackerProduct', $initiative->getPHID()) ->executeOne(); $account = PhortuneAccountQuery::loadActiveAccountForUser( $viewer, PhabricatorContentSource::newFromRequest($request)); $cart_implementation = id(new FundBackerCart()) ->setInitiative($initiative); - $cart = $account->newCart($viewer, $cart_implementation); + $cart = $account->newCart($viewer, $cart_implementation, $merchant); $purchase = $cart->newPurchase($viewer, $product); $purchase ->setBasePriceAsCurrency($currency) ->setMetadataValue('backerPHID', $backer->getPHID()) ->save(); $xactions = array(); $xactions[] = id(new FundBackerTransaction()) ->setTransactionType(FundBackerTransaction::TYPE_STATUS) ->setNewValue(FundBacker::STATUS_IN_CART); $editor = id(new FundBackerEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request); $editor->applyTransactions($backer, $xactions); $cart->activateCart(); return id(new AphrontRedirectResponse()) ->setURI($cart->getCheckoutURI()); } } $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('amount') ->setLabel(pht('Amount')) ->setValue($v_amount) ->setError($e_amount)); return $this->newDialog() ->setTitle(pht('Back Initiative')) ->setErrors($errors) ->appendChild($form->buildLayoutView()) ->addCancelButton($initiative_uri) ->addSubmitButton(pht('Continue')); } } diff --git a/src/applications/fund/controller/FundInitiativeEditController.php b/src/applications/fund/controller/FundInitiativeEditController.php index 292f21b1a1..381bb88f9a 100644 --- a/src/applications/fund/controller/FundInitiativeEditController.php +++ b/src/applications/fund/controller/FundInitiativeEditController.php @@ -1,191 +1,247 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($this->id) { $initiative = id(new FundInitiativeQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$initiative) { return new Aphront404Response(); } $is_new = false; } else { $initiative = FundInitiative::initializeNewInitiative($viewer); $is_new = true; } if ($is_new) { $title = pht('Create Initiative'); $button_text = pht('Create Initiative'); $cancel_uri = $this->getApplicationURI(); } else { $title = pht( 'Edit %s %s', $initiative->getMonogram(), $initiative->getName()); $button_text = pht('Save Changes'); $cancel_uri = '/'.$initiative->getMonogram(); } $e_name = true; $v_name = $initiative->getName(); + $e_merchant = null; + $v_merchant = $initiative->getMerchantPHID(); + $v_desc = $initiative->getDescription(); if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $initiative->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } $validation_exception = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); $v_desc = $request->getStr('description'); $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); + $v_merchant = $request->getStr('merchantPHID'); $v_projects = $request->getArr('projects'); $type_name = FundInitiativeTransaction::TYPE_NAME; $type_desc = FundInitiativeTransaction::TYPE_DESCRIPTION; + $type_merchant = FundInitiativeTransaction::TYPE_MERCHANT; $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $xactions = array(); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_desc) ->setNewValue($v_desc); + $xactions[] = id(new FundInitiativeTransaction()) + ->setTransactionType($type_merchant) + ->setNewValue($v_merchant); + $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_view) ->setNewValue($v_view); $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType($type_edit) ->setNewValue($v_edit); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new FundInitiativeTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new FundInitiativeEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($initiative, $xactions); return id(new AphrontRedirectResponse()) ->setURI('/'.$initiative->getMonogram()); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_name = $ex->getShortMessage($type_name); + $e_merchant = $ex->getShortMessage($type_merchant); $initiative->setViewPolicy($v_view); $initiative->setEditPolicy($v_edit); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($initiative) ->execute(); if ($v_projects) { $project_handles = $this->loadViewerHandles($v_projects); } else { $project_handles = array(); } + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + + $merchant_options = array(); + foreach ($merchants as $merchant) { + $merchant_options[$merchant->getPHID()] = pht( + 'Merchant %d %s', + $merchant->getID(), + $merchant->getName()); + } + + if ($v_merchant && empty($merchant_options[$v_merchant])) { + $merchant_options = array( + $v_merchant => pht('(Restricted Merchant)'), + ) + $merchant_options; + } + + if (!$merchant_options) { + return $this->newDialog() + ->setTitle(pht('No Valid Phortune Merchant Accounts')) + ->appendParagraph( + pht( + 'You do not control any merchant accounts which can receive '. + 'payments from this initiative. When you create an initiative, '. + 'you need to specify a merchant account where funds will be paid '. + 'to.')) + ->appendParagraph( + pht( + 'Create a merchant account in the Phortune application before '. + 'creating an initiative in Fund.')) + ->addCancelButton($this->getApplicationURI()); + } + $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') ->setLabel(pht('Name')) ->setValue($v_name) ->setError($e_name)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName('merchantPHID') + ->setLabel(pht('Pay To Merchant')) + ->setValue($v_merchant) + ->setError($e_merchant) + ->setOptions($merchant_options)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('description') ->setLabel(pht('Description')) ->setValue($v_desc)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Projects')) ->setName('projects') ->setValue($project_handles) ->setDatasource(new PhabricatorProjectDatasource())) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($initiative) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($initiative) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button_text) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $crumbs->addTextCrumb(pht('Create Initiative')); } else { $crumbs->addTextCrumb( $initiative->getMonogram(), '/'.$initiative->getMonogram()); $crumbs->addTextCrumb(pht('Edit')); } $box = id(new PHUIObjectBoxView()) ->setValidationException($validation_exception) ->setHeaderText($title) ->appendChild($form); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $title, )); } } diff --git a/src/applications/fund/editor/FundInitiativeEditor.php b/src/applications/fund/editor/FundInitiativeEditor.php index e33f601a80..39df92e51e 100644 --- a/src/applications/fund/editor/FundInitiativeEditor.php +++ b/src/applications/fund/editor/FundInitiativeEditor.php @@ -1,133 +1,181 @@ getTransactionType()) { case FundInitiativeTransaction::TYPE_NAME: return $object->getName(); case FundInitiativeTransaction::TYPE_DESCRIPTION: return $object->getDescription(); case FundInitiativeTransaction::TYPE_STATUS: return $object->getStatus(); case FundInitiativeTransaction::TYPE_BACKER: return null; + case FundInitiativeTransaction::TYPE_MERCHANT: + return $object->getMerchantPHID(); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case FundInitiativeTransaction::TYPE_NAME: case FundInitiativeTransaction::TYPE_DESCRIPTION: case FundInitiativeTransaction::TYPE_STATUS: case FundInitiativeTransaction::TYPE_BACKER: + case FundInitiativeTransaction::TYPE_MERCHANT: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case FundInitiativeTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; case FundInitiativeTransaction::TYPE_DESCRIPTION: $object->setDescription($xaction->getNewValue()); return; + case FundInitiativeTransaction::TYPE_MERCHANT: + $object->setMerchantPHID($xaction->getNewValue()); + return; case FundInitiativeTransaction::TYPE_STATUS: $object->setStatus($xaction->getNewValue()); return; case FundInitiativeTransaction::TYPE_BACKER: // TODO: Calculate total funding / backers / etc. return; case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_EDGE: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case FundInitiativeTransaction::TYPE_NAME: case FundInitiativeTransaction::TYPE_DESCRIPTION: case FundInitiativeTransaction::TYPE_STATUS: + case FundInitiativeTransaction::TYPE_MERCHANT: case FundInitiativeTransaction::TYPE_BACKER: // TODO: Maybe we should apply the backer transaction from here? return; case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_EDGE: return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); switch ($type) { case FundInitiativeTransaction::TYPE_NAME: $missing = $this->validateIsEmptyTextField( $object->getName(), $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Initiative name is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } break; + case FundInitiativeTransaction::TYPE_MERCHANT: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Payable merchant is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } else if ($xactions) { + $merchant_phid = last($xactions)->getNewValue(); + + // Make sure the actor has permission to edit the merchant they're + // selecting. You aren't allowed to send payments to an account you + // do not control. + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($this->requireActor()) + ->withPHIDs(array($merchant_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + if (!$merchants) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'You must specify a merchant account you control as the '. + 'recipient of funds from this initiative.'), + last($xactions)); + $errors[] = $error; + } + } + break; } return $errors; } } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index e0cb4b5fa8..edf3d6407a 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -1,171 +1,173 @@ pht('Open'), self::STATUS_CLOSED => pht('Closed'), ); } public static function initializeNewInitiative(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorFundApplication')) ->executeOne(); $view_policy = $app->getPolicy(FundDefaultViewCapability::CAPABILITY); return id(new FundInitiative()) ->setOwnerPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($actor->getPHID()) ->setStatus(self::STATUS_OPEN); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'description' => 'text', 'status' => 'text32', + 'merchantPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( 'columns' => array('status'), ), 'key_owner' => array( 'columns' => array('ownerPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(FundInitiativePHIDType::TYPECONST); } public function getMonogram() { return 'I'.$this->getID(); } public function getProjectPHIDs() { return $this->assertAttached($this->projectPHIDs); } public function attachProjectPHIDs(array $phids) { $this->projectPHIDs = $phids; return $this; } public function isClosed() { return ($this->getStatus() == self::STATUS_CLOSED); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getOwnerPHID()); } public function describeAutomaticCapability($capability) { return pht( 'The owner of an initiative can always view and edit it.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new FundInitiativeEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new FundInitiativeTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getOwnerPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenRecevierInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getOwnerPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/fund/storage/FundInitiativeTransaction.php b/src/applications/fund/storage/FundInitiativeTransaction.php index e1cca16bef..30eb458060 100644 --- a/src/applications/fund/storage/FundInitiativeTransaction.php +++ b/src/applications/fund/storage/FundInitiativeTransaction.php @@ -1,146 +1,182 @@ getOldValue(); + $new = $this->getNewValue(); + + $type = $this->getTransactionType(); + switch ($type) { + case FundInitiativeTransaction::TYPE_MERCHANT: + if ($old) { + $phids[] = $old; + } + if ($new) { + $phids[] = $new; + } + break; + } + + return $phids; + } + public function getTitle() { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case FundInitiativeTransaction::TYPE_NAME: if ($old === null) { return pht( '%s created this initiative.', $this->renderHandleLink($author_phid)); } else { return pht( '%s renamed this initiative from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); } break; case FundInitiativeTransaction::TYPE_DESCRIPTION: return pht( '%s edited the description of this initiative.', $this->renderHandleLink($author_phid)); case FundInitiativeTransaction::TYPE_STATUS: switch ($new) { case FundInitiative::STATUS_OPEN: return pht( '%s reopened this initiative.', $this->renderHandleLink($author_phid)); case FundInitiative::STATUS_CLOSED: return pht( '%s closed this initiative.', $this->renderHandleLink($author_phid)); } break; case FundInitiativeTransaction::TYPE_BACKER: return pht( '%s backed this initiative.', $this->renderHandleLink($author_phid)); + case FundInitiativeTransaction::TYPE_MERCHANT: + if ($old === null) { + return pht( + '%s set this initiative to pay to %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($new)); + } else { + return pht( + '%s changed the merchant receiving funds from this '. + 'initiative from %s to %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); + } } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); $type = $this->getTransactionType(); switch ($type) { case FundInitiativeTransaction::TYPE_NAME: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s renamed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; case FundInitiativeTransaction::TYPE_DESCRIPTION: return pht( '%s updated the description for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case FundInitiativeTransaction::TYPE_STATUS: switch ($new) { case FundInitiative::STATUS_OPEN: return pht( '%s reopened %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case FundInitiative::STATUS_CLOSED: return pht( '%s closed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } break; case FundInitiativeTransaction::TYPE_BACKER: return pht( '%s backed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return parent::getTitleForFeed($story); } public function shouldHide() { $old = $this->getOldValue(); switch ($this->getTransactionType()) { case FundInitiativeTransaction::TYPE_DESCRIPTION: return ($old === null); } return parent::shouldHide(); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case FundInitiativeTransaction::TYPE_DESCRIPTION: return ($this->getOldValue() !== null); } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } } diff --git a/src/applications/phortune/controller/PhortuneAccountViewController.php b/src/applications/phortune/controller/PhortuneAccountViewController.php index 3b89443bea..1017f05ee1 100644 --- a/src/applications/phortune/controller/PhortuneAccountViewController.php +++ b/src/applications/phortune/controller/PhortuneAccountViewController.php @@ -1,276 +1,270 @@ accountID = $data['accountID']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $account = id(new PhortuneAccountQuery()) ->setViewer($user) ->withIDs(array($this->accountID)) ->executeOne(); if (!$account) { return new Aphront404Response(); } $title = $account->getName(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Account'), $request->getRequestURI()); $header = id(new PHUIHeaderView()) ->setHeader($title); $actions = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($request->getRequestURI()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Account')) ->setIcon('fa-pencil') ->setHref('#') ->setDisabled(true)) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Members')) ->setIcon('fa-users') ->setHref('#') ->setDisabled(true)); $crumbs->setActionList($actions); $properties = id(new PHUIPropertyListView()) ->setObject($account) ->setUser($user); $properties->addProperty(pht('Balance'), '-'); $properties->setActionList($actions); $payment_methods = $this->buildPaymentMethodsSection($account); $purchase_history = $this->buildPurchaseHistorySection($account); $charge_history = $this->buildChargeHistorySection($account); $account_history = $this->buildAccountHistorySection($account); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); return $this->buildApplicationPage( array( $crumbs, $object_box, $payment_methods, $purchase_history, $charge_history, $account_history, ), array( 'title' => $title, )); } private function buildPaymentMethodsSection(PhortuneAccount $account) { $request = $this->getRequest(); $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $account, PhabricatorPolicyCapability::CAN_EDIT); $id = $account->getID(); $header = id(new PHUIHeaderView()) - ->setHeader(pht('Payment Methods')) - ->addActionLink( - id(new PHUIButtonView()) - ->setTag('a') - ->setHref($this->getApplicationURI($id.'/card/new/')) - ->setText(pht('Add Payment Method')) - ->setIcon(id(new PHUIIconView())->setIconFont('fa-plus'))); + ->setHeader(pht('Payment Methods')); $list = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht('No payment methods associated with this account.')); $methods = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) ->execute(); if ($methods) { $this->loadHandles(mpull($methods, 'getAuthorPHID')); } foreach ($methods as $method) { $id = $method->getID(); $item = new PHUIObjectItemView(); $item->setHeader($method->getFullDisplayName()); switch ($method->getStatus()) { case PhortunePaymentMethod::STATUS_ACTIVE: $item->setBarColor('green'); $disable_uri = $this->getApplicationURI('card/'.$id.'/disable/'); $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->setHref($disable_uri) ->setDisabled(!$can_edit) ->setWorkflow(true)); break; case PhortunePaymentMethod::STATUS_DISABLED: $item->setDisabled(true); break; } $provider = $method->buildPaymentProvider(); $item->addAttribute($provider->getPaymentMethodProviderDescription()); $item->setImageURI($provider->getPaymentMethodIcon()); $edit_uri = $this->getApplicationURI('card/'.$id.'/edit/'); $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-pencil') ->setHref($edit_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $list->addItem($item); } return id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($list); } private function buildPurchaseHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $viewer = $request->getUser(); $carts = id(new PhortuneCartQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) ->needPurchases(true) ->withStatuses( array( PhortuneCart::STATUS_PURCHASING, PhortuneCart::STATUS_PURCHASED, )) ->execute(); $rows = array(); $rowc = array(); foreach ($carts as $cart) { $cart_link = phutil_tag( 'a', array( 'href' => $this->getApplicationURI('cart/'.$cart->getID().'/'), ), pht('Cart %d', $cart->getID())); $rowc[] = 'highlighted'; $rows[] = array( phutil_tag('strong', array(), $cart_link), '', '', ); foreach ($cart->getPurchases() as $purchase) { $id = $purchase->getID(); $price = $purchase->getTotalPriceAsCurrency()->formatForDisplay(); $purchase_link = phutil_tag( 'a', array( 'href' => $this->getApplicationURI('purchase/'.$id.'/'), ), $purchase->getFullDisplayName()); $rowc[] = ''; $rows[] = array( '', $purchase_link, $price, ); } } $table = id(new AphrontTableView($rows)) ->setRowClasses($rowc) ->setHeaders( array( pht('Cart'), pht('Purchase'), pht('Amount'), )) ->setColumnClasses( array( '', 'wide', 'right', )); $header = id(new PHUIHeaderView()) ->setHeader(pht('Purchase History')); return id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($table); } private function buildChargeHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $viewer = $request->getUser(); $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) ->needCarts(true) ->execute(); return $this->buildChargesTable($charges); } private function buildAccountHistorySection(PhortuneAccount $account) { $request = $this->getRequest(); $user = $request->getUser(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Account History')); $xactions = id(new PhortuneAccountTransactionQuery()) ->setViewer($user) ->withObjectPHIDs(array($account->getPHID())) ->execute(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($user); $xaction_view = id(new PhabricatorApplicationTransactionView()) ->setUser($user) ->setObjectPHID($account->getPHID()) ->setTransactions($xactions) ->setMarkupEngine($engine); $box = id(new PHUIObjectBoxView()) ->setHeader($header); return array( $box, $xaction_view, ); } } diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php index 81b1d70b1a..6284c3a04b 100644 --- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php @@ -1,224 +1,234 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $cart = id(new PhortuneCartQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needPurchases(true) ->executeOne(); if (!$cart) { return new Aphront404Response(); } $cancel_uri = $cart->getCancelURI(); + $merchant = $cart->getMerchant(); switch ($cart->getStatus()) { case PhortuneCart::STATUS_BUILDING: return $this->newDialog() ->setTitle(pht('Incomplete Cart')) ->appendParagraph( pht( 'The application that created this cart did not finish putting '. 'products in it. You can not checkout with an incomplete '. 'cart.')) ->addCancelButton($cancel_uri); case PhortuneCart::STATUS_READY: // This is the expected, normal state for a cart that's ready for // checkout. break; case PhortuneCart::STATUS_PURCHASING: // We've started the purchase workflow for this cart, but were not able // to complete it. If the workflow is on an external site, this could // happen because the user abandoned the workflow. Just return them to // the right place so they can resume where they left off. $uri = $cart->getMetadataValue('provider.checkoutURI'); if ($uri !== null) { return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($uri); } return $this->newDialog() ->setTitle(pht('Charge Failed')) ->appendParagraph( pht( 'Failed to charge this cart.')) ->addCancelButton($cancel_uri); break; case PhortuneCart::STATUS_CHARGED: // TODO: This is really bad (we took your money and at least partially // failed to fulfill your order) and should have better steps forward. return $this->newDialog() ->setTitle(pht('Purchase Failed')) ->appendParagraph( pht( 'This cart was charged but the purchase could not be '. 'completed.')) ->addCancelButton($cancel_uri); case PhortuneCart::STATUS_PURCHASED: return id(new AphrontRedirectResponse())->setURI($cart->getDetailURI()); default: throw new Exception( pht( 'Unknown cart status "%s"!', $cart->getStatus())); } $account = $cart->getAccount(); $account_uri = $this->getApplicationURI($account->getID().'/'); $methods = id(new PhortunePaymentMethodQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) + ->withMerchantPHIDs(array($merchant->getPHID())) ->withStatuses(array(PhortunePaymentMethod::STATUS_ACTIVE)) ->execute(); $e_method = null; $errors = array(); if ($request->isFormPost()) { // Require CAN_EDIT on the cart to actually make purchases. PhabricatorPolicyFilter::requireCapability( $viewer, $cart, PhabricatorPolicyCapability::CAN_EDIT); $method_id = $request->getInt('paymentMethodID'); $method = idx($methods, $method_id); if (!$method) { $e_method = pht('Required'); $errors[] = pht('You must choose a payment method.'); } if (!$errors) { $provider = $method->buildPaymentProvider(); $charge = $cart->willApplyCharge($viewer, $provider, $method); $provider->applyCharge($method, $charge); $cart->didApplyCharge($charge); $done_uri = $cart->getDoneURI(); return id(new AphrontRedirectResponse())->setURI($done_uri); } } $cart_box = $this->buildCartContents($cart); $cart_box->setFormErrors($errors); $title = pht('Buy Stuff'); if (!$methods) { $method_control = id(new AphrontFormStaticControl()) ->setLabel(pht('Payment Method')) ->setValue( phutil_tag('em', array(), pht('No payment methods configured.'))); } else { $method_control = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Payment Method')) ->setName('paymentMethodID') ->setValue($request->getInt('paymentMethodID')); foreach ($methods as $method) { $method_control->addButton( $method->getID(), $method->getFullDisplayName(), $method->getDescription()); } } $method_control->setError($e_method); - $payment_method_uri = $this->getApplicationURI( - $account->getID().'/card/new/'); + $account_id = $account->getID(); + + $payment_method_uri = $this->getApplicationURI("{$account_id}/card/new/"); + $payment_method_uri = new PhutilURI($payment_method_uri); + $payment_method_uri->setQueryParams( + array( + 'merchantID' => $merchant->getID(), + 'cartID' => $cart->getID(), + )); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($method_control); - $add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); + $add_providers = $this->loadCreatePaymentMethodProvidersForMerchant( + $merchant); if ($add_providers) { $new_method = phutil_tag( 'a', array( 'class' => 'button grey', 'href' => $payment_method_uri, 'sigil' => 'workflow', ), pht('Add New Payment Method')); $form->appendChild( id(new AphrontFormMarkupControl()) ->setValue($new_method)); } if ($methods || $add_providers) { $submit = id(new AphrontFormSubmitControl()) ->setValue(pht('Submit Payment')) ->setDisabled(!$methods); if ($cart->getCancelURI() !== null) { $submit->addCancelButton($cart->getCancelURI()); } $form->appendChild($submit); } $provider_form = null; - $pay_providers = PhortunePaymentProvider::getProvidersForOneTimePayment(); + $pay_providers = $this->loadOneTimePaymentProvidersForMerchant($merchant); if ($pay_providers) { $one_time_options = array(); foreach ($pay_providers as $provider) { $one_time_options[] = $provider->renderOneTimePaymentButton( $account, $cart, $viewer); } $one_time_options = phutil_tag( 'div', array( 'class' => 'phortune-payment-onetime-list', ), $one_time_options); $provider_form = new PHUIFormLayoutView(); $provider_form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Pay With') ->setValue($one_time_options)); } $payment_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Choose Payment Method')) ->appendChild($form) ->appendChild($provider_form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $cart_box, $payment_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phortune/controller/PhortuneController.php b/src/applications/phortune/controller/PhortuneController.php index 587e3c3110..9195b18c10 100644 --- a/src/applications/phortune/controller/PhortuneController.php +++ b/src/applications/phortune/controller/PhortuneController.php @@ -1,89 +1,135 @@ getRequest())); } protected function buildChargesTable(array $charges, $show_cart = true) { $request = $this->getRequest(); $viewer = $request->getUser(); $rows = array(); foreach ($charges as $charge) { $cart = $charge->getCart(); $cart_id = $cart->getID(); $cart_uri = $this->getApplicationURI("cart/{$cart_id}/"); $cart_href = phutil_tag( 'a', array( 'href' => $cart_uri, ), pht('Cart %d', $cart_id)); $rows[] = array( $charge->getID(), $cart_href, $charge->getPaymentProviderKey(), $charge->getPaymentMethodPHID(), $charge->getAmountAsCurrency()->formatForDisplay(), $charge->getStatus(), phabricator_datetime($charge->getDateCreated(), $viewer), ); } $charge_table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('ID'), pht('Cart'), pht('Provider'), pht('Method'), pht('Amount'), pht('Status'), pht('Created'), )) ->setColumnClasses( array( '', 'strong', '', '', 'wide right', '', '', )) ->setColumnVisibility( array( true, $show_cart, )); $header = id(new PHUIHeaderView()) ->setHeader(pht('Charge History')); return id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($charge_table); } protected function addAccountCrumb( $crumbs, PhortuneAccount $account, $link = true) { $name = pht('Account'); $href = null; if ($link) { $href = $this->getApplicationURI($account->getID().'/'); $crumbs->addTextCrumb($name, $href); } else { $crumbs->addTextCrumb($name); } } + private function loadEnabledProvidersForMerchant(PhortuneMerchant $merchant) { + $viewer = $this->getRequest()->getUser(); + + $provider_configs = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($viewer) + ->withMerchantPHIDs(array($merchant->getPHID())) + ->execute(); + $providers = mpull($provider_configs, 'buildProvider', 'getID'); + + foreach ($providers as $key => $provider) { + if (!$provider->isEnabled()) { + unset($providers[$key]); + } + } + + return $providers; + } + + protected function loadCreatePaymentMethodProvidersForMerchant( + PhortuneMerchant $merchant) { + + $providers = $this->loadEnabledProvidersForMerchant($merchant); + foreach ($providers as $key => $provider) { + if (!$provider->canCreatePaymentMethods()) { + unset($providers[$key]); + continue; + } + } + + return $providers; + } + + protected function loadOneTimePaymentProvidersForMerchant( + PhortuneMerchant $merchant) { + + $providers = $this->loadEnabledProvidersForMerchant($merchant); + foreach ($providers as $key => $provider) { + if (!$provider->canProcessOneTimePayments()) { + unset($providers[$key]); + continue; + } + } + + return $providers; + } + } diff --git a/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php index 76c8e8ff03..043ee82ff2 100644 --- a/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php +++ b/src/applications/phortune/controller/PhortunePaymentMethodCreateController.php @@ -1,233 +1,251 @@ accountID = $data['accountID']; } public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $account = id(new PhortuneAccountQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($this->accountID)) ->executeOne(); if (!$account) { return new Aphront404Response(); } + $merchant = id(new PhortuneMerchantQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getInt('merchantID'))) + ->executeOne(); + if (!$merchant) { + return new Aphront404Response(); + } + $cancel_uri = $this->getApplicationURI($account->getID().'/'); $account_uri = $this->getApplicationURI($account->getID().'/'); - $providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); + $providers = $this->loadCreatePaymentMethodProvidersForMerchant($merchant); if (!$providers) { throw new Exception( 'There are no payment providers enabled that can add payment '. 'methods.'); } - $provider_key = $request->getStr('providerKey'); - if (empty($providers[$provider_key])) { + $provider_id = $request->getInt('providerID'); + if (empty($providers[$provider_id])) { $choices = array(); foreach ($providers as $provider) { $choices[] = $this->renderSelectProvider($provider); } $content = phutil_tag( 'div', array( 'class' => 'phortune-payment-method-list', ), $choices); return $this->newDialog() ->setRenderDialogAsDiv(true) ->setTitle(pht('Add Payment Method')) ->appendParagraph(pht('Choose a payment method to add:')) ->appendChild($content) ->addCancelButton($account_uri); } - $provider = $providers[$provider_key]; + $provider = $providers[$provider_id]; $errors = array(); if ($request->isFormPost() && $request->getBool('isProviderForm')) { $method = id(new PhortunePaymentMethod()) ->setAccountPHID($account->getPHID()) - ->setAuthorPHID($user->getPHID()) - ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE) - ->setProviderType($provider->getProviderType()) - ->setProviderDomain($provider->getProviderDomain()); + ->setAuthorPHID($viewer->getPHID()) + ->setMerchantPHID($merchant->getPHID()) + ->setProviderPHID($provider->getProviderConfig()->getPHID()) + ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); if (!$errors) { $errors = $this->processClientErrors( $provider, $request->getStr('errors')); } if (!$errors) { $client_token_raw = $request->getStr('token'); $client_token = json_decode($client_token_raw, true); if (!is_array($client_token)) { $errors[] = pht( 'There was an error decoding token information submitted by the '. 'client. Expected a JSON-encoded token dictionary, received: %s.', nonempty($client_token_raw, pht('nothing'))); } else { if (!$provider->validateCreatePaymentMethodToken($client_token)) { $errors[] = pht( 'There was an error with the payment token submitted by the '. 'client. Expected a valid dictionary, received: %s.', $client_token_raw); } } if (!$errors) { $errors = $provider->createPaymentMethodFromRequest( $request, $method, $client_token); } } if (!$errors) { $method->save(); - $save_uri = new PhutilURI($account_uri); - $save_uri->setFragment('payment'); - return id(new AphrontRedirectResponse())->setURI($save_uri); + // If we added this method on a cart flow, return to the cart to + // check out. + $cart_id = $request->getInt('cartID'); + if ($cart_id) { + $next_uri = $this->getApplicationURI( + "cart/{$cart_id}/checkout/?paymentMethodID=".$method->getID()); + } else { + $next_uri = new PhutilURI($account_uri); + $next_uri->setFragment('payment'); + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); } else { $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->setTitle(pht('Error Adding Payment Method')) ->appendChild(id(new AphrontErrorView())->setErrors($errors)) ->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } $form = $provider->renderCreatePaymentMethodForm($request, $errors); $form - ->setUser($user) + ->setUser($viewer) ->setAction($request->getRequestURI()) ->setWorkflow(true) - ->addHiddenInput('providerKey', $provider_key) + ->addHiddenInput('providerID', $provider_id) + ->addHiddenInput('cartID', $request->getInt('cartID')) ->addHiddenInput('isProviderForm', true) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Add Payment Method')) ->addCancelButton($account_uri)); $box = id(new PHUIObjectBoxView()) ->setHeaderText($provider->getPaymentMethodDescription()) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Add Payment Method')); return $this->buildApplicationPage( array( $crumbs, $box, ), array( 'title' => $provider->getPaymentMethodDescription(), )); } private function renderSelectProvider( PhortunePaymentProvider $provider) { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $description = $provider->getPaymentMethodDescription(); $icon_uri = $provider->getPaymentMethodIcon(); $details = $provider->getPaymentMethodProviderDescription(); $this->requireResource('phortune-css'); $icon = id(new PHUIIconView()) ->setImage($icon_uri) ->addClass('phortune-payment-icon'); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($description) ->setSubtext($details); $form = id(new AphrontFormView()) - ->setUser($user) - ->addHiddenInput('providerKey', $provider->getProviderKey()) + ->setUser($viewer) + ->addHiddenInput('providerID', $provider->getProviderConfig()->getID()) ->appendChild($button); return $form; } private function processClientErrors( PhortunePaymentProvider $provider, $client_errors_raw) { $errors = array(); $client_errors = json_decode($client_errors_raw, true); if (!is_array($client_errors)) { $errors[] = pht( 'There was an error decoding error information submitted by the '. 'client. Expected a JSON-encoded list of error codes, received: %s.', nonempty($client_errors_raw, pht('nothing'))); } foreach (array_unique($client_errors) as $key => $client_error) { $client_errors[$key] = $provider->translateCreatePaymentMethodErrorCode( $client_error); } foreach (array_unique($client_errors) as $client_error) { switch ($client_error) { case PhortuneErrCode::ERR_CC_INVALID_NUMBER: $message = pht( 'The card number you entered is not a valid card number. Check '. 'that you entered it correctly.'); break; case PhortuneErrCode::ERR_CC_INVALID_CVC: $message = pht( 'The CVC code you entered is not a valid CVC code. Check that '. 'you entered it correctly. The CVC code is a 3-digit or 4-digit '. 'numeric code which usually appears on the back of the card.'); break; case PhortuneErrCode::ERR_CC_INVALID_EXPIRY: $message = pht( 'The card expiration date is not a valid expiration date. Check '. 'that you entered it correctly. You can not add an expired card '. 'as a payment method.'); break; default: $message = $provider->getCreatePaymentMethodErrorMessage( $client_error); if (!$message) { $message = pht( "There was an unexpected error ('%s') processing payment ". "information.", $client_error); phlog($message); } break; } $errors[$client_error] = $message; } return $errors; } } diff --git a/src/applications/phortune/phid/PhortuneMerchantPHIDType.php b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php index 8d6273e2a5..143a970fbf 100644 --- a/src/applications/phortune/phid/PhortuneMerchantPHIDType.php +++ b/src/applications/phortune/phid/PhortuneMerchantPHIDType.php @@ -1,38 +1,38 @@ withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $merchant = $objects[$phid]; $id = $merchant->getID(); - $handle->setName(pht('Merchant %d', $id)); + $handle->setName(pht('Merchant %d %s', $id, $merchant->getName())); $handle->setURI("/phortune/merchant/{$id}/"); } } } diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index b4c9d80606..3874752ede 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -1,310 +1,280 @@ providerConfig = $provider_config; return $this; } public function getProviderConfig() { return $this->providerConfig; } /** * Return a short name which identifies this provider. */ abstract public function getName(); /* -( Configuring Providers )---------------------------------------------- */ /** * Return a human-readable provider name for use on the merchant workflow * where a merchant owner adds providers. */ abstract public function getConfigureName(); /** * Return a human-readable provider description for use on the merchant * workflow where a merchant owner adds providers. */ abstract public function getConfigureDescription(); abstract public function getConfigureInstructions(); abstract public function getAllConfigurableProperties(); abstract public function getAllConfigurableSecretProperties(); /** * Read a dictionary of properties from the provider's configuration for * use when editing the provider. */ public function readEditFormValuesFromProviderConfig() { $properties = $this->getAllConfigurableProperties(); $config = $this->getProviderConfig(); $secrets = $this->getAllConfigurableSecretProperties(); $secrets = array_fuse($secrets); $map = array(); foreach ($properties as $property) { $map[$property] = $config->getMetadataValue($property); if (isset($secrets[$property])) { $map[$property] = $this->renderConfigurationSecret($map[$property]); } } return $map; } /** * Read a dictionary of properties from a request for use when editing the * provider. */ public function readEditFormValuesFromRequest(AphrontRequest $request) { $properties = $this->getAllConfigurableProperties(); $map = array(); foreach ($properties as $property) { $map[$property] = $request->getStr($property); } return $map; } abstract public function processEditForm( AphrontRequest $request, array $values); abstract public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues); protected function renderConfigurationSecret($value) { if (strlen($value)) { return str_repeat('*', strlen($value)); } return ''; } public function isConfigurationSecret($value) { return preg_match('/^\*+\z/', trim($value)); } abstract public function canRunConfigurationTest(); public function runConfigurationTest() { throw new PhortuneNotImplementedException($this); } /* -( Selecting Providers )------------------------------------------------ */ public static function getAllProviders() { return id(new PhutilSymbolLoader()) ->setAncestorClass('PhortunePaymentProvider') ->loadObjects(); } - public static function getEnabledProviders() { - $providers = self::getAllProviders(); - foreach ($providers as $key => $provider) { - if (!$provider->isEnabled()) { - unset($providers[$key]); - } - } - return $providers; - } - - public static function getProvidersForAddPaymentMethod() { - $providers = self::getEnabledProviders(); - foreach ($providers as $key => $provider) { - if (!$provider->canCreatePaymentMethods()) { - unset($providers[$key]); - } - } - return $providers; - } - - public static function getProvidersForOneTimePayment() { - $providers = self::getEnabledProviders(); - foreach ($providers as $key => $provider) { - if (!$provider->canProcessOneTimePayments()) { - unset($providers[$key]); - } - } - return $providers; - } - abstract public function isEnabled(); abstract public function getPaymentMethodDescription(); abstract public function getPaymentMethodIcon(); abstract public function getPaymentMethodProviderDescription(); final public function applyCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { $this->executeCharge($payment_method, $charge); } abstract protected function executeCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge); /* -( Adding Payment Methods )--------------------------------------------- */ /** * @task addmethod */ public function canCreatePaymentMethods() { return false; } /** * @task addmethod */ public function translateCreatePaymentMethodErrorCode($error_code) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function getCreatePaymentMethodErrorMessage($error_code) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function validateCreatePaymentMethodToken(array $token) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function createPaymentMethodFromRequest( AphrontRequest $request, PhortunePaymentMethod $method, array $token) { throw new PhortuneNotImplementedException($this); } /** * @task addmethod */ public function renderCreatePaymentMethodForm( AphrontRequest $request, array $errors) { throw new PhortuneNotImplementedException($this); } public function getDefaultPaymentMethodDisplayName( PhortunePaymentMethod $method) { throw new PhortuneNotImplementedException($this); } /* -( One-Time Payments )-------------------------------------------------- */ public function canProcessOneTimePayments() { return false; } public function renderOneTimePaymentButton( PhortuneAccount $account, PhortuneCart $cart, PhabricatorUser $user) { require_celerity_resource('phortune-css'); $icon_uri = $this->getPaymentMethodIcon(); $description = $this->getPaymentMethodProviderDescription(); $details = $this->getPaymentMethodDescription(); $icon = id(new PHUIIconView()) ->setImage($icon_uri) ->addClass('phortune-payment-icon'); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($description) ->setSubtext($details); // NOTE: We generate a local URI to make sure the form picks up CSRF tokens. $uri = $this->getControllerURI( 'checkout', array( 'cartID' => $cart->getID(), ), $local = true); return phabricator_form( $user, array( 'action' => $uri, 'method' => 'POST', ), $button); } /* -( Controllers )-------------------------------------------------------- */ final public function getControllerURI( $action, array $params = array(), $local = false) { $id = $this->getProviderConfig()->getID(); $app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication'); $path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/'; $uri = new PhutilURI($path); $uri->setQueryParams($params); if ($local) { return $uri; } else { return PhabricatorEnv::getURI((string)$uri); } } public function canRespondToControllerAction($action) { return false; } public function processControllerRequest( PhortuneProviderActionController $controller, AphrontRequest $request) { throw new PhortuneNotImplementedException($this); } } diff --git a/src/applications/phortune/query/PhortuneCartQuery.php b/src/applications/phortune/query/PhortuneCartQuery.php index c81deb4fb8..b0e86c9715 100644 --- a/src/applications/phortune/query/PhortuneCartQuery.php +++ b/src/applications/phortune/query/PhortuneCartQuery.php @@ -1,147 +1,162 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAccountPHIDs(array $account_phids) { $this->accountPHIDs = $account_phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function needPurchases($need_purchases) { $this->needPurchases = $need_purchases; return $this; } protected function loadPage() { $table = new PhortuneCart(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT cart.* FROM %T cart %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $carts) { $accounts = id(new PhortuneAccountQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($carts, 'getAccountPHID')) ->execute(); $accounts = mpull($accounts, null, 'getPHID'); foreach ($carts as $key => $cart) { $account = idx($accounts, $cart->getAccountPHID()); if (!$account) { unset($carts[$key]); continue; } $cart->attachAccount($account); } + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(mpull($carts, 'getMerchantPHID')) + ->execute(); + $merchants = mpull($merchants, null, 'getPHID'); + + foreach ($carts as $key => $cart) { + $merchant = idx($merchants, $cart->getMerchantPHID()); + if (!$merchant) { + unset($carts[$key]); + continue; + } + $cart->attachMerchant($merchant); + } + $implementations = array(); $cart_map = mgroup($carts, 'getCartClass'); foreach ($cart_map as $class => $class_carts) { $implementations += newv($class, array())->loadImplementationsForCarts( $this->getViewer(), $class_carts); } foreach ($carts as $key => $cart) { $implementation = idx($implementations, $key); if (!$implementation) { unset($carts[$key]); continue; } $cart->attachImplementation($implementation); } return $carts; } protected function didFilterPage(array $carts) { if ($this->needPurchases) { $purchases = id(new PhortunePurchaseQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withCartPHIDs(mpull($carts, 'getPHID')) ->execute(); $purchases = mgroup($purchases, 'getCartPHID'); foreach ($carts as $cart) { $cart->attachPurchases(idx($purchases, $cart->getPHID(), array())); } } return $carts; } private function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'cart.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'cart.phid IN (%Ls)', $this->phids); } if ($this->accountPHIDs !== null) { $where[] = qsprintf( $conn, 'cart.accountPHID IN (%Ls)', $this->accountPHIDs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'cart.status IN (%Ls)', $this->statuses); } return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php index c6d7606062..87455e4bd9 100644 --- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php +++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php @@ -1,114 +1,148 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withAccountPHIDs(array $phids) { $this->accountPHIDs = $phids; return $this; } + public function withMerchantPHIDs(array $phids) { + $this->merchantPHIDs = $phids; + return $this; + } + public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } protected function loadPage() { $table = new PhortunePaymentMethod(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $methods) { - foreach ($methods as $key => $method) { - try { - $method->buildPaymentProvider(); - } catch (Exception $ex) { - unset($methods[$key]); - continue; - } - } - $accounts = id(new PhortuneAccountQuery()) ->setViewer($this->getViewer()) ->withPHIDs(mpull($methods, 'getAccountPHID')) ->execute(); $accounts = mpull($accounts, null, 'getPHID'); foreach ($methods as $key => $method) { $account = idx($accounts, $method->getAccountPHID()); if (!$account) { unset($methods[$key]); continue; } $method->attachAccount($account); } + $merchants = id(new PhortuneMerchantQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(mpull($methods, 'getMerchantPHID')) + ->execute(); + $merchants = mpull($merchants, null, 'getPHID'); + + foreach ($methods as $key => $method) { + $merchant = idx($merchants, $method->getMerchantPHID()); + if (!$merchant) { + unset($methods[$key]); + continue; + } + $method->attachMerchant($merchant); + } + + $provider_configs = id(new PhortunePaymentProviderConfigQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(mpull($methods, 'getProviderPHID')) + ->execute(); + $provider_configs = mpull($provider_configs, null, 'getPHID'); + + foreach ($methods as $key => $method) { + $provider_config = idx($provider_configs, $method->getProviderPHID()); + if (!$provider_config) { + unset($methods[$key]); + continue; + } + $method->attachProviderConfig($provider_config); + } + return $methods; } private function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->accountPHIDs !== null) { $where[] = qsprintf( $conn, 'accountPHID IN (%Ls)', $this->accountPHIDs); } + if ($this->merchantPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'merchantPHID IN (%Ls)', + $this->merchantPHIDs); + } + if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'status IN (%Ls)', $this->statuses); } $where[] = $this->buildPagingClause($conn); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/storage/PhortuneAccount.php b/src/applications/phortune/storage/PhortuneAccount.php index 78c7f47dd7..c22fa90db7 100644 --- a/src/applications/phortune/storage/PhortuneAccount.php +++ b/src/applications/phortune/storage/PhortuneAccount.php @@ -1,126 +1,127 @@ memberPHIDs = array(); return $account; } public static function createNewAccount( PhabricatorUser $actor, PhabricatorContentSource $content_source) { $account = PhortuneAccount::initializeNewAccount($actor); $xactions = array(); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhortuneAccountTransaction::TYPE_NAME) ->setNewValue(pht('Account (%s)', $actor->getUserName())); $xactions[] = id(new PhortuneAccountTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue( 'edge:type', PhabricatorEdgeConfig::TYPE_ACCOUNT_HAS_MEMBER) ->setNewValue( array( '=' => array($actor->getPHID() => $actor->getPHID()), )); $editor = id(new PhortuneAccountEditor()) ->setActor($actor) ->setContentSource($content_source); // We create an account for you the first time you visit Phortune. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $editor->applyTransactions($account, $xactions); unset($unguarded); return $account; } public function newCart( PhabricatorUser $actor, - PhortuneCartImplementation $implementation) { + PhortuneCartImplementation $implementation, + PhortuneMerchant $merchant) { - $cart = PhortuneCart::initializeNewCart($actor, $this); + $cart = PhortuneCart::initializeNewCart($actor, $this, $merchant); $cart->setCartClass(get_class($implementation)); $cart->attachImplementation($implementation); $implementation->willCreateCart($actor, $cart); return $cart->save(); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneAccountPHIDType::TYPECONST); } public function getMemberPHIDs() { return $this->assertAttached($this->memberPHIDs); } public function attachMemberPHIDs(array $phids) { $this->memberPHIDs = $phids; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($this->getPHID() === null) { // Allow a user to create an account for themselves. return PhabricatorPolicies::POLICY_USER; } else { return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $members = array_fuse($this->getMemberPHIDs()); return isset($members[$viewer->getPHID()]); } public function describeAutomaticCapability($capability) { return pht('Members of an account can always view and edit it.'); } } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 0afa6b07d1..e2c84f0d3e 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -1,236 +1,253 @@ setAuthorPHID($actor->getPHID()) ->setStatus(self::STATUS_BUILDING) - ->setAccountPHID($account->getPHID()); + ->setAccountPHID($account->getPHID()) + ->setMerchantPHID($merchant->getPHID()); $cart->account = $account; $cart->purchases = array(); return $cart; } public function newPurchase( PhabricatorUser $actor, PhortuneProduct $product) { $purchase = PhortunePurchase::initializeNewPurchase($actor, $product) ->setAccountPHID($this->getAccount()->getPHID()) ->setCartPHID($this->getPHID()) ->save(); $this->purchases[] = $purchase; return $purchase; } public function activateCart() { $this->setStatus(self::STATUS_READY)->save(); return $this; } public function willApplyCharge( PhabricatorUser $actor, PhortunePaymentProvider $provider, PhortunePaymentMethod $method = null) { $account = $this->getAccount(); $charge = PhortuneCharge::initializeNewCharge() ->setAccountPHID($account->getPHID()) ->setCartPHID($this->getPHID()) ->setAuthorPHID($actor->getPHID()) - ->setPaymentProviderKey($provider->getProviderKey()) + ->setMerchantPHID($this->getMerchant()->getPHID()) + ->setProviderPHID($provider->getProviderConfig()->getPHID()) ->setAmountAsCurrency($this->getTotalPriceAsCurrency()); if ($method) { $charge->setPaymentMethodPHID($method->getPHID()); } $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_READY) { throw new Exception( pht( 'Cart has wrong status ("%s") to call willApplyCharge(), expected '. '"%s".', $copy->getStatus(), self::STATUS_READY)); } $charge->save(); $this->setStatus(PhortuneCart::STATUS_PURCHASING)->save(); $this->saveTransaction(); return $charge; } public function didApplyCharge(PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_CHARGED); $this->openTransaction(); $this->beginReadLocking(); $copy = clone $this; $copy->reload(); if ($copy->getStatus() !== self::STATUS_PURCHASING) { throw new Exception( pht( 'Cart has wrong status ("%s") to call didApplyCharge(), expected '. '"%s".', $copy->getStatus(), self::STATUS_PURCHASING)); } $charge->save(); $this->setStatus(self::STATUS_CHARGED)->save(); $this->saveTransaction(); foreach ($this->purchases as $purchase) { $purchase->getProduct()->didPurchaseProduct($purchase); } $this->setStatus(self::STATUS_PURCHASED)->save(); return $this; } public function getDoneURI() { return $this->getImplementation()->getDoneURI($this); } public function getCancelURI() { return $this->getImplementation()->getCancelURI($this); } public function getDetailURI() { return '/phortune/cart/'.$this->getID().'/'; } public function getCheckoutURI() { return '/phortune/cart/'.$this->getID().'/checkout/'; } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'cartClass' => 'text128', ), self::CONFIG_KEY_SCHEMA => array( 'key_account' => array( 'columns' => array('accountPHID'), ), + 'key_merchant' => array( + 'columns' => array('merchantPHID'), + ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneCartPHIDType::TYPECONST); } public function attachPurchases(array $purchases) { assert_instances_of($purchases, 'PhortunePurchase'); $this->purchases = $purchases; return $this; } public function getPurchases() { return $this->assertAttached($this->purchases); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getAccount() { return $this->assertAttached($this->account); } + public function attachMerchant(PhortuneMerchant $merchant) { + $this->merchant = $merchant; + return $this; + } + + public function getMerchant() { + return $this->assertAttached($this->merchant); + } + public function attachImplementation( PhortuneCartImplementation $implementation) { $this->implementation = $implementation; return $this; } public function getImplementation() { return $this->assertAttached($this->implementation); } public function getTotalPriceAsCurrency() { $prices = array(); foreach ($this->getPurchases() as $purchase) { $prices[] = $purchase->getTotalPriceAsCurrency(); } return PhortuneCurrency::newFromList($prices); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getAccount()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getAccount()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Carts inherit the policies of the associated account.'); } } diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php index ebabc2708e..6386c3db7e 100644 --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -1,113 +1,120 @@ setStatus(self::STATUS_CHARGING); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_APPLICATION_SERIALIZERS => array( 'amountAsCurrency' => new PhortuneCurrencySerializer(), ), self::CONFIG_COLUMN_SCHEMA => array( 'paymentProviderKey' => 'text128', 'paymentMethodPHID' => 'phid?', 'amountAsCurrency' => 'text64', 'status' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_cart' => array( 'columns' => array('cartPHID'), ), 'key_account' => array( 'columns' => array('accountPHID'), ), + 'key_merchant' => array( + 'columns' => array('merchantPHID'), + ), + 'key_provider' => array( + 'columns' => array('providerPHID'), + ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortuneChargePHIDType::TYPECONST); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getAccount() { return $this->assertAttached($this->account); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getCart() { return $this->assertAttached($this->cart); } public function attachCart(PhortuneCart $cart = null) { $this->cart = $cart; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getAccount()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getAccount()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Charges inherit the policies of the associated account.'); } } diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php index a5cc6b3b66..573f4b6c97 100644 --- a/src/applications/phortune/storage/PhortunePaymentMethod.php +++ b/src/applications/phortune/storage/PhortunePaymentMethod.php @@ -1,133 +1,157 @@ true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'status' => 'text64', 'brand' => 'text64', 'expires' => 'text16', 'lastFourDigits' => 'text16', ), self::CONFIG_KEY_SCHEMA => array( 'key_account' => array( 'columns' => array('accountPHID', 'status'), ), + 'key_merchant' => array( + 'columns' => array('merchantPHID', 'accountPHID'), + ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhortunePaymentMethodPHIDType::TYPECONST); } public function attachAccount(PhortuneAccount $account) { $this->account = $account; return $this; } public function getAccount() { return $this->assertAttached($this->account); } + public function attachMerchant(PhortuneMerchant $merchant) { + $this->merchant = $merchant; + return $this; + } + + public function getMerchant() { + return $this->assertAttached($this->merchant); + } + + public function attachProviderConfig(PhortunePaymentProviderConfig $config) { + $this->providerConfig = $config; + return $this; + } + + public function getProviderConfig() { + return $this->assertAttached($this->providerConfig); + } + public function getDescription() { $provider = $this->buildPaymentProvider(); return $provider->getPaymentMethodProviderDescription(); } public function getMetadataValue($key, $default = null) { return idx($this->getMetadata(), $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function buildPaymentProvider() { - throw new Exception(pht('TODO: Reimplement this junk.')); + return $this->getProviderConfig()->buildProvider(); } public function getDisplayName() { if (strlen($this->name)) { return $this->name; } $provider = $this->buildPaymentProvider(); return $provider->getDefaultPaymentMethodDisplayName($this); } public function getFullDisplayName() { return pht('%s (%s)', $this->getDisplayName(), $this->getSummary()); } public function getSummary() { return pht('%s %s', $this->getBrand(), $this->getLastFourDigits()); } public function setExpires($year, $month) { $this->expires = $year.'-'.$month; return $this; } public function getDisplayExpires() { list($year, $month) = explode('-', $this->getExpires()); $month = sprintf('%02d', $month); $year = substr($year, -2); return $month.'/'.$year; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getAccount()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getAccount()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Members of an account can always view and edit its payment methods.'); } }