Page MenuHomePhorge

No OneTemporary

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 @@
<?php
final class FundInitiativeBackController
extends FundController {
private $id;
public function willProcessRequest(array $data) {
$this->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 @@
<?php
final class FundInitiativeEditController
extends FundController {
private $id;
public function willProcessRequest(array $data) {
$this->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 @@
<?php
final class FundInitiativeEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorFundApplication';
}
public function getEditorObjectsDescription() {
return pht('Fund Initiatives');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = FundInitiativeTransaction::TYPE_NAME;
$types[] = FundInitiativeTransaction::TYPE_DESCRIPTION;
$types[] = FundInitiativeTransaction::TYPE_STATUS;
$types[] = FundInitiativeTransaction::TYPE_BACKER;
+ $types[] = FundInitiativeTransaction::TYPE_MERCHANT;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->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 @@
<?php
final class FundInitiative extends FundDAO
implements
PhabricatorPolicyInterface,
PhabricatorProjectInterface,
PhabricatorApplicationTransactionInterface,
PhabricatorSubscribableInterface,
PhabricatorMentionableInterface,
PhabricatorFlaggableInterface,
PhabricatorTokenReceiverInterface,
PhabricatorDestructibleInterface {
protected $name;
protected $ownerPHID;
+ protected $merchantPHID;
protected $description;
protected $viewPolicy;
protected $editPolicy;
protected $status;
private $projectPHIDs = self::ATTACHABLE;
const STATUS_OPEN = 'open';
const STATUS_CLOSED = 'closed';
public static function getStatusNameMap() {
return array(
self::STATUS_OPEN => 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 @@
<?php
final class FundInitiativeTransaction
extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'fund:name';
const TYPE_DESCRIPTION = 'fund:description';
const TYPE_STATUS = 'fund:status';
const TYPE_BACKER = 'fund:backer';
+ const TYPE_MERCHANT = 'fund:merchant';
public function getApplicationName() {
return 'fund';
}
public function getApplicationTransactionType() {
return FundInitiativePHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
+ public function getRequiredHandlePHIDs() {
+ $phids = parent::getRequiredHandlePHIDs();
+
+ $old = $this->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 @@
<?php
final class PhortuneAccountViewController extends PhortuneController {
private $accountID;
public function willProcessRequest(array $data) {
$this->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 @@
<?php
final class PhortuneCartCheckoutController
extends PhortuneCartController {
private $id;
public function willProcessRequest(array $data) {
$this->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 @@
<?php
abstract class PhortuneController extends PhabricatorController {
protected function loadActiveAccount(PhabricatorUser $user) {
return PhortuneAccountQuery::loadActiveAccountForUser(
$user,
PhabricatorContentSource::newFromRequest($this->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 @@
<?php
final class PhortunePaymentMethodCreateController
extends PhortuneController {
private $accountID;
public function willProcessRequest(array $data) {
$this->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 @@
<?php
final class PhortuneMerchantPHIDType extends PhabricatorPHIDType {
const TYPECONST = 'PMRC';
public function getTypeName() {
return pht('Phortune Merchant');
}
public function newObject() {
return new PhortuneMerchant();
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhortuneMerchantQuery())
->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 @@
<?php
/**
* @task addmethod Adding Payment Methods
*/
abstract class PhortunePaymentProvider {
private $providerConfig;
public function setProviderConfig(
PhortunePaymentProviderConfig $provider_config) {
$this->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 @@
<?php
final class PhortuneCartQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
private $statuses;
private $needPurchases;
public function withIDs(array $ids) {
$this->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 @@
<?php
final class PhortunePaymentMethodQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
+ private $merchantPHIDs;
private $statuses;
public function withIDs(array $ids) {
$this->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 @@
<?php
/**
* An account represents a purchasing entity. An account may have multiple users
* on it (e.g., several employees of a company have access to the company
* account), and a user may have several accounts (e.g., a company account and
* a personal account).
*/
final class PhortuneAccount extends PhortuneDAO
implements PhabricatorPolicyInterface {
protected $name;
private $memberPHIDs = self::ATTACHABLE;
public static function initializeNewAccount(PhabricatorUser $actor) {
$account = id(new PhortuneAccount());
$account->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 @@
<?php
final class PhortuneCart extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
protected $authorPHID;
+ protected $merchantPHID;
protected $cartClass;
protected $status;
protected $metadata = array();
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
+ private $merchant = self::ATTACHABLE;
public static function initializeNewCart(
PhabricatorUser $actor,
- PhortuneAccount $account) {
+ PhortuneAccount $account,
+ PhortuneMerchant $merchant) {
$cart = id(new PhortuneCart())
->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 @@
<?php
/**
* A charge is a charge (or credit) against an account and represents an actual
* transfer of funds. Each charge is normally associated with a cart, but a
* cart may have multiple charges. For example, a product may have a failed
* charge followed by a successful charge.
*/
final class PhortuneCharge extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_CHARGING = 'charge:charging';
const STATUS_CHARGED = 'charge:charged';
const STATUS_FAILED = 'charge:failed';
protected $accountPHID;
protected $authorPHID;
protected $cartPHID;
- protected $paymentProviderKey;
+ protected $providerPHID;
+ protected $merchantPHID;
protected $paymentMethodPHID;
protected $amountAsCurrency;
protected $status;
protected $metadata = array();
private $account = self::ATTACHABLE;
private $cart = self::ATTACHABLE;
public static function initializeNewCharge() {
return id(new PhortuneCharge())
->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 @@
<?php
/**
* A payment method is a credit card; it is associated with an account and
* charges can be made against it.
*/
final class PhortunePaymentMethod extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_ACTIVE = 'payment:active';
const STATUS_DISABLED = 'payment:disabled';
protected $name = '';
protected $status;
protected $accountPHID;
protected $authorPHID;
+ protected $merchantPHID;
protected $providerPHID;
protected $expires;
protected $metadata = array();
protected $brand;
protected $lastFourDigits;
private $account = self::ATTACHABLE;
+ private $merchant = self::ATTACHABLE;
+ private $providerConfig = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => 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.');
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 19:55 (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1128200
Default Alt Text
(96 KB)

Event Timeline