mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-18 12:52:42 +01:00
Mostly implement order refunds and cancellations
Summary: Ref T2787. This has some rough edges but basically works. - Users can cancel orders that are in incomplete states (or in complete states, if the application allows them to -- for example, some future application might allow cancellation of billed-but-not-shipped orders). - Merchant controllers can partially or fully refund orders from any state after payment. Test Plan: This is still rough around the edges, but issued Stripe and WePay refunds. Reviewers: btrahan Reviewed By: btrahan Subscribers: chad, epriestley Maniphest Tasks: T2787 Differential Revision: https://secure.phabricator.com/D10664
This commit is contained in:
parent
b6c65719e4
commit
1e8c314c81
23 changed files with 781 additions and 57 deletions
11
resources/sql/autopatches/20141008.phortunerefund.sql
Normal file
11
resources/sql/autopatches/20141008.phortunerefund.sql
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
ALTER TABLE {$NAMESPACE}_phortune.phortune_charge
|
||||||
|
ADD amountRefundedAsCurrency VARCHAR(64) NOT NULL COLLATE utf8_bin;
|
||||||
|
|
||||||
|
UPDATE {$NAMESPACE}_phortune.phortune_charge
|
||||||
|
SET amountRefundedAsCurrency = '0.00 USD';
|
||||||
|
|
||||||
|
ALTER TABLE {$NAMESPACE}_phortune.phortune_charge
|
||||||
|
ADD refundingPHID VARBINARY(64);
|
||||||
|
|
||||||
|
ALTER TABLE {$NAMESPACE}_phortune.phortune_charge
|
||||||
|
ADD refundedChargePHID VARBINARY(64);
|
|
@ -2554,6 +2554,7 @@ phutil_register_library_map(array(
|
||||||
'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
|
'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php',
|
||||||
'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
|
'PhortuneBalancedPaymentProvider' => 'applications/phortune/provider/PhortuneBalancedPaymentProvider.php',
|
||||||
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
|
'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php',
|
||||||
|
'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php',
|
||||||
'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php',
|
'PhortuneCartCheckoutController' => 'applications/phortune/controller/PhortuneCartCheckoutController.php',
|
||||||
'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php',
|
'PhortuneCartController' => 'applications/phortune/controller/PhortuneCartController.php',
|
||||||
'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
|
'PhortuneCartImplementation' => 'applications/phortune/cart/PhortuneCartImplementation.php',
|
||||||
|
@ -5608,6 +5609,7 @@ phutil_register_library_map(array(
|
||||||
'PhortuneDAO',
|
'PhortuneDAO',
|
||||||
'PhabricatorPolicyInterface',
|
'PhabricatorPolicyInterface',
|
||||||
),
|
),
|
||||||
|
'PhortuneCartCancelController' => 'PhortuneCartController',
|
||||||
'PhortuneCartCheckoutController' => 'PhortuneCartController',
|
'PhortuneCartCheckoutController' => 'PhortuneCartController',
|
||||||
'PhortuneCartController' => 'PhortuneController',
|
'PhortuneCartController' => 'PhortuneController',
|
||||||
'PhortuneCartListController' => 'PhortuneController',
|
'PhortuneCartListController' => 'PhortuneController',
|
||||||
|
|
|
@ -115,4 +115,11 @@ final class FundBackerProduct extends PhortuneProductImplementation {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function didRefundProduct(
|
||||||
|
PhortuneProduct $product,
|
||||||
|
PhortunePurchase $purchase) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
// TODO: Undonate.
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
|
||||||
'cart/(?P<id>\d+)/' => array(
|
'cart/(?P<id>\d+)/' => array(
|
||||||
'' => 'PhortuneCartViewController',
|
'' => 'PhortuneCartViewController',
|
||||||
'checkout/' => 'PhortuneCartCheckoutController',
|
'checkout/' => 'PhortuneCartCheckoutController',
|
||||||
|
'(?P<action>cancel|refund)/' => 'PhortuneCartCancelController',
|
||||||
),
|
),
|
||||||
'account/' => array(
|
'account/' => array(
|
||||||
'' => 'PhortuneAccountListController',
|
'' => 'PhortuneAccountListController',
|
||||||
|
|
|
@ -16,6 +16,21 @@ abstract class PhortuneCartImplementation {
|
||||||
abstract public function getCancelURI(PhortuneCart $cart);
|
abstract public function getCancelURI(PhortuneCart $cart);
|
||||||
abstract public function getDoneURI(PhortuneCart $cart);
|
abstract public function getDoneURI(PhortuneCart $cart);
|
||||||
|
|
||||||
|
public function assertCanCancelOrder(PhortuneCart $cart) {
|
||||||
|
switch ($cart->getStatus()) {
|
||||||
|
case PhortuneCart::STATUS_PURCHASED:
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'This order can not be cancelled because it has already been '.
|
||||||
|
'completed.'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assertCanRefundOrder(PhortuneCart $cart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
abstract public function willCreateCart(
|
abstract public function willCreateCart(
|
||||||
PhabricatorUser $viewer,
|
PhabricatorUser $viewer,
|
||||||
PhortuneCart $cart);
|
PhortuneCart $cart);
|
||||||
|
|
|
@ -12,9 +12,20 @@ final class PhortuneAccountViewController extends PhortuneController {
|
||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
$user = $request->getUser();
|
$user = $request->getUser();
|
||||||
|
|
||||||
|
// TODO: Currently, you must be able to edit an account to view the detail
|
||||||
|
// page, because the account must be broadly visible so merchants can
|
||||||
|
// process orders but merchants should not be able to see all the details
|
||||||
|
// of an account. Ideally this page should be visible to merchants, too,
|
||||||
|
// just with less information.
|
||||||
|
|
||||||
$account = id(new PhortuneAccountQuery())
|
$account = id(new PhortuneAccountQuery())
|
||||||
->setViewer($user)
|
->setViewer($user)
|
||||||
->withIDs(array($this->accountID))
|
->withIDs(array($this->accountID))
|
||||||
|
->requireCapabilities(
|
||||||
|
array(
|
||||||
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT,
|
||||||
|
))
|
||||||
->executeOne();
|
->executeOne();
|
||||||
|
|
||||||
if (!$account) {
|
if (!$account) {
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhortuneCartCancelController
|
||||||
|
extends PhortuneCartController {
|
||||||
|
|
||||||
|
private $id;
|
||||||
|
private $action;
|
||||||
|
|
||||||
|
public function willProcessRequest(array $data) {
|
||||||
|
$this->id = $data['id'];
|
||||||
|
$this->action = $data['action'];
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($this->action) {
|
||||||
|
case 'cancel':
|
||||||
|
// You must be able to edit the account to cancel an order.
|
||||||
|
PhabricatorPolicyFilter::requireCapability(
|
||||||
|
$viewer,
|
||||||
|
$cart->getAccount(),
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
$is_refund = false;
|
||||||
|
break;
|
||||||
|
case 'refund':
|
||||||
|
// You must be able to control the merchant to refund an order.
|
||||||
|
PhabricatorPolicyFilter::requireCapability(
|
||||||
|
$viewer,
|
||||||
|
$cart->getMerchant(),
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
$is_refund = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$cancel_uri = $cart->getDetailURI();
|
||||||
|
$merchant = $cart->getMerchant();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($is_refund) {
|
||||||
|
$title = pht('Unable to Refund Order');
|
||||||
|
$cart->assertCanRefundOrder();
|
||||||
|
} else {
|
||||||
|
$title = pht('Unable to Cancel Order');
|
||||||
|
$cart->assertCanCancelOrder();
|
||||||
|
}
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
return $this->newDialog()
|
||||||
|
->setTitle($title)
|
||||||
|
->appendChild($ex->getMessage())
|
||||||
|
->addCancelButton($cancel_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
$charges = id(new PhortuneChargeQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withCartPHIDs(array($cart->getPHID()))
|
||||||
|
->withStatuses(
|
||||||
|
array(
|
||||||
|
PhortuneCharge::STATUS_CHARGED,
|
||||||
|
))
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$amounts = mpull($charges, 'getAmountAsCurrency');
|
||||||
|
$maximum = PhortuneCurrency::newFromList($amounts);
|
||||||
|
$v_refund = $maximum->formatForDisplay();
|
||||||
|
|
||||||
|
$errors = array();
|
||||||
|
$e_refund = true;
|
||||||
|
if ($request->isFormPost()) {
|
||||||
|
if ($is_refund) {
|
||||||
|
try {
|
||||||
|
$refund = PhortuneCurrency::newFromUserInput(
|
||||||
|
$viewer,
|
||||||
|
$request->getStr('refund'));
|
||||||
|
$refund->assertInRange('0.00 USD', $maximum->formatForDisplay());
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$errors[] = $ex;
|
||||||
|
$e_refund = pht('Invalid');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$refund = $maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
$charges = msort($charges, 'getID');
|
||||||
|
$charges = array_reverse($charges);
|
||||||
|
|
||||||
|
if ($charges) {
|
||||||
|
$providers = id(new PhortunePaymentProviderConfigQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(mpull($charges, 'getProviderPHID'))
|
||||||
|
->execute();
|
||||||
|
$providers = mpull($providers, null, 'getPHID');
|
||||||
|
} else {
|
||||||
|
$providers = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($charges as $charge) {
|
||||||
|
$refundable = $charge->getAmountRefundableAsCurrency();
|
||||||
|
if (!$refundable->isPositive()) {
|
||||||
|
// This charge is a refund, or has already been fully refunded.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($refund->isGreaterThan($refundable)) {
|
||||||
|
$refund_amount = $refundable;
|
||||||
|
} else {
|
||||||
|
$refund_amount = $refund;
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider_config = idx($providers, $charge->getProviderPHID());
|
||||||
|
if (!$provider_config) {
|
||||||
|
throw new Exception(pht('Unable to load provider for charge!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = $provider_config->buildProvider();
|
||||||
|
|
||||||
|
$refund_charge = $cart->willRefundCharge(
|
||||||
|
$viewer,
|
||||||
|
$provider,
|
||||||
|
$charge,
|
||||||
|
$refund_amount);
|
||||||
|
|
||||||
|
$refunded = false;
|
||||||
|
try {
|
||||||
|
$provider->refundCharge($charge, $refund_charge);
|
||||||
|
$refunded = true;
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
phlog($ex);
|
||||||
|
$cart->didFailRefund($charge, $refund_charge);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($refunded) {
|
||||||
|
$cart->didRefundCharge($charge, $refund_charge);
|
||||||
|
$refund = $refund->subtract($refund_amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$refund->isPositive()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($refund->isPositive()) {
|
||||||
|
throw new Exception(pht('Unable to refund some charges!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_refund) {
|
||||||
|
$title = pht('Refund Order?');
|
||||||
|
$body = pht(
|
||||||
|
'Really refund this order?');
|
||||||
|
$button = pht('Refund Order');
|
||||||
|
|
||||||
|
$form = id(new AphrontFormView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormTextControl())
|
||||||
|
->setName('refund')
|
||||||
|
->setLabel(pht('Amount'))
|
||||||
|
->setError($e_refund)
|
||||||
|
->setValue($v_refund));
|
||||||
|
|
||||||
|
$form = $form->buildLayoutView();
|
||||||
|
} else {
|
||||||
|
$title = pht('Cancel Order?');
|
||||||
|
$body = pht(
|
||||||
|
'Really cancel this order? Any payment will be refunded.');
|
||||||
|
$button = pht('Cancel Order');
|
||||||
|
|
||||||
|
$form = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->newDialog()
|
||||||
|
->setTitle($title)
|
||||||
|
->appendChild($body)
|
||||||
|
->appendChild($form)
|
||||||
|
->addSubmitButton($button)
|
||||||
|
->addCancelButton($cancel_uri);
|
||||||
|
}
|
||||||
|
}
|
|
@ -119,8 +119,12 @@ final class PhortuneCartCheckoutController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$cart_box = $this->buildCartContents($cart);
|
$cart_table = $this->buildCartContentTable($cart);
|
||||||
$cart_box->setFormErrors($errors);
|
|
||||||
|
$cart_box = id(new PHUIObjectBoxView())
|
||||||
|
->setFormErrors($errors)
|
||||||
|
->setHeaderText(pht('Cart Contents'))
|
||||||
|
->appendChild($cart_table);
|
||||||
|
|
||||||
$title = pht('Buy Stuff');
|
$title = pht('Buy Stuff');
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
abstract class PhortuneCartController
|
abstract class PhortuneCartController
|
||||||
extends PhortuneController {
|
extends PhortuneController {
|
||||||
|
|
||||||
protected function buildCartContents(PhortuneCart $cart) {
|
protected function buildCartContentTable(PhortuneCart $cart) {
|
||||||
|
|
||||||
$rows = array();
|
$rows = array();
|
||||||
foreach ($cart->getPurchases() as $purchase) {
|
foreach ($cart->getPurchases() as $purchase) {
|
||||||
|
@ -39,9 +39,7 @@ abstract class PhortuneCartController
|
||||||
'right',
|
'right',
|
||||||
));
|
));
|
||||||
|
|
||||||
return id(new PHUIObjectBoxView())
|
return $table;
|
||||||
->setHeaderText(pht('Cart Contents'))
|
|
||||||
->appendChild($table);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,26 @@ final class PhortuneCartViewController
|
||||||
return new Aphront404Response();
|
return new Aphront404Response();
|
||||||
}
|
}
|
||||||
|
|
||||||
$cart_box = $this->buildCartContents($cart);
|
$can_admin = PhabricatorPolicyFilter::hasCapability(
|
||||||
|
$viewer,
|
||||||
|
$cart->getMerchant(),
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
|
||||||
|
$cart_table = $this->buildCartContentTable($cart);
|
||||||
|
|
||||||
|
$properties = $this->buildPropertyListView($cart);
|
||||||
|
$actions = $this->buildActionListView($cart, $can_admin);
|
||||||
|
$properties->setActionList($actions);
|
||||||
|
|
||||||
|
$header = id(new PHUIHeaderView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->setHeader(pht('Order Detail'))
|
||||||
|
->setPolicyObject($cart);
|
||||||
|
|
||||||
|
$cart_box = id(new PHUIObjectBoxView())
|
||||||
|
->setHeader($header)
|
||||||
|
->appendChild($properties)
|
||||||
|
->appendChild($cart_table);
|
||||||
|
|
||||||
$charges = id(new PhortuneChargeQuery())
|
$charges = id(new PhortuneChargeQuery())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
|
@ -49,4 +68,80 @@ final class PhortuneCartViewController
|
||||||
));
|
));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildPropertyListView(PhortuneCart $cart) {
|
||||||
|
|
||||||
|
$viewer = $this->getRequest()->getUser();
|
||||||
|
|
||||||
|
$view = id(new PHUIPropertyListView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->setObject($cart);
|
||||||
|
|
||||||
|
$handles = $this->loadViewerHandles(
|
||||||
|
array(
|
||||||
|
$cart->getAccountPHID(),
|
||||||
|
$cart->getAuthorPHID(),
|
||||||
|
$cart->getMerchantPHID(),
|
||||||
|
));
|
||||||
|
|
||||||
|
$view->addProperty(
|
||||||
|
pht('Order Name'),
|
||||||
|
$cart->getName());
|
||||||
|
$view->addProperty(
|
||||||
|
pht('Account'),
|
||||||
|
$handles[$cart->getAccountPHID()]->renderLink());
|
||||||
|
$view->addProperty(
|
||||||
|
pht('Authorized By'),
|
||||||
|
$handles[$cart->getAuthorPHID()]->renderLink());
|
||||||
|
$view->addProperty(
|
||||||
|
pht('Merchant'),
|
||||||
|
$handles[$cart->getMerchantPHID()]->renderLink());
|
||||||
|
$view->addProperty(
|
||||||
|
pht('Status'),
|
||||||
|
PhortuneCart::getNameForStatus($cart->getStatus()));
|
||||||
|
$view->addProperty(
|
||||||
|
pht('Updated'),
|
||||||
|
phabricator_datetime($cart->getDateModified(), $viewer));
|
||||||
|
|
||||||
|
return $view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildActionListView(PhortuneCart $cart, $can_admin) {
|
||||||
|
$viewer = $this->getRequest()->getUser();
|
||||||
|
$id = $cart->getID();
|
||||||
|
|
||||||
|
$can_edit = PhabricatorPolicyFilter::hasCapability(
|
||||||
|
$viewer,
|
||||||
|
$cart,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
|
||||||
|
$view = id(new PhabricatorActionListView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->setObject($cart);
|
||||||
|
|
||||||
|
$can_cancel = ($can_edit && $cart->canCancelOrder());
|
||||||
|
|
||||||
|
$cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/");
|
||||||
|
$refund_uri = $this->getApplicationURI("cart/{$id}/refund/");
|
||||||
|
|
||||||
|
$view->addAction(
|
||||||
|
id(new PhabricatorActionView())
|
||||||
|
->setName(pht('Cancel Order'))
|
||||||
|
->setIcon('fa-times')
|
||||||
|
->setDisabled(!$can_cancel)
|
||||||
|
->setWorkflow(true)
|
||||||
|
->setHref($cancel_uri));
|
||||||
|
|
||||||
|
if ($can_admin) {
|
||||||
|
$view->addAction(
|
||||||
|
id(new PhabricatorActionView())
|
||||||
|
->setName(pht('Refund Order'))
|
||||||
|
->setIcon('fa-reply')
|
||||||
|
->setWorkflow(true)
|
||||||
|
->setHref($refund_uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $view;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,12 @@ abstract class PhortuneController extends PhabricatorController {
|
||||||
$charge->getID(),
|
$charge->getID(),
|
||||||
$handles[$charge->getCartPHID()]->renderLink(),
|
$handles[$charge->getCartPHID()]->renderLink(),
|
||||||
$handles[$charge->getProviderPHID()]->renderLink(),
|
$handles[$charge->getProviderPHID()]->renderLink(),
|
||||||
$handles[$charge->getPaymentMethodPHID()]->renderLink(),
|
$charge->getPaymentMethodPHID()
|
||||||
|
? $handles[$charge->getPaymentMethodPHID()]->renderLink()
|
||||||
|
: null,
|
||||||
$handles[$charge->getMerchantPHID()]->renderLink(),
|
$handles[$charge->getMerchantPHID()]->renderLink(),
|
||||||
$charge->getAmountAsCurrency()->formatForDisplay(),
|
$charge->getAmountAsCurrency()->formatForDisplay(),
|
||||||
PhortuneCharge::getNameForStatus($charge->getStatus()),
|
$charge->getStatusForDisplay(),
|
||||||
phabricator_datetime($charge->getDateCreated(), $viewer),
|
phabricator_datetime($charge->getDateCreated(), $viewer),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,14 +48,12 @@ final class PhortuneCurrency extends Phobject {
|
||||||
$value = (int)round(100 * $value);
|
$value = (int)round(100 * $value);
|
||||||
|
|
||||||
$currency = idx($matches, 2, $default);
|
$currency = idx($matches, 2, $default);
|
||||||
if ($currency) {
|
|
||||||
switch ($currency) {
|
switch ($currency) {
|
||||||
case 'USD':
|
case 'USD':
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Exception("Unsupported currency '{$currency}'!");
|
throw new Exception("Unsupported currency '{$currency}'!");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return self::newFromValueAndCurrency($value, $currency);
|
return self::newFromValueAndCurrency($value, $currency);
|
||||||
}
|
}
|
||||||
|
@ -126,9 +124,17 @@ final class PhortuneCurrency extends Phobject {
|
||||||
throw new Exception("Invalid currency format ('{$string}').");
|
throw new Exception("Invalid currency format ('{$string}').");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function throwUnlikeCurrenciesException(PhortuneCurrency $other) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Trying to operate on unlike currencies ("%s" and "%s")!',
|
||||||
|
$this->currency,
|
||||||
|
$other->currency));
|
||||||
|
}
|
||||||
|
|
||||||
public function add(PhortuneCurrency $other) {
|
public function add(PhortuneCurrency $other) {
|
||||||
if ($this->currency !== $other->currency) {
|
if ($this->currency !== $other->currency) {
|
||||||
throw new Exception(pht('Trying to add unlike currencies!'));
|
$this->throwUnlikeCurrenciesException($other);
|
||||||
}
|
}
|
||||||
|
|
||||||
$currency = new PhortuneCurrency();
|
$currency = new PhortuneCurrency();
|
||||||
|
@ -140,6 +146,46 @@ final class PhortuneCurrency extends Phobject {
|
||||||
return $currency;
|
return $currency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function subtract(PhortuneCurrency $other) {
|
||||||
|
if ($this->currency !== $other->currency) {
|
||||||
|
$this->throwUnlikeCurrenciesException($other);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currency = new PhortuneCurrency();
|
||||||
|
|
||||||
|
// TODO: This should check for integer overflows, etc.
|
||||||
|
$currency->value = $this->value - $other->value;
|
||||||
|
$currency->currency = $this->currency;
|
||||||
|
|
||||||
|
return $currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqualTo(PhortuneCurrency $other) {
|
||||||
|
if ($this->currency !== $other->currency) {
|
||||||
|
$this->throwUnlikeCurrenciesException($other);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($this->value === $other->value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function negate() {
|
||||||
|
$currency = new PhortuneCurrency();
|
||||||
|
$currency->value = -$this->value;
|
||||||
|
$currency->currency = $this->currency;
|
||||||
|
return $currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPositive() {
|
||||||
|
return ($this->value > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isGreaterThan(PhortuneCurrency $other) {
|
||||||
|
if ($this->currency !== $other->currency) {
|
||||||
|
$this->throwUnlikeCurrenciesException($other);
|
||||||
|
}
|
||||||
|
return $this->value > $other->value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that a currency value lies within a range.
|
* Assert that a currency value lies within a range.
|
||||||
*
|
*
|
||||||
|
|
|
@ -28,4 +28,10 @@ abstract class PhortuneProductImplementation {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function didRefundProduct(
|
||||||
|
PhortuneProduct $product,
|
||||||
|
PhortunePurchase $purchase) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,13 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
|
||||||
$charge->save();
|
$charge->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function executeRefund(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
// TODO: Implement.
|
||||||
|
throw new PhortuneNotImplementedException($this);
|
||||||
|
}
|
||||||
|
|
||||||
private function getMarketplaceID() {
|
private function getMarketplaceID() {
|
||||||
return $this
|
return $this
|
||||||
->getProviderConfig()
|
->getProviderConfig()
|
||||||
|
@ -192,7 +199,7 @@ final class PhortuneBalancedPaymentProvider extends PhortunePaymentProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getMarketplaceURI() {
|
private function getMarketplaceURI() {
|
||||||
return '/v1/marketplace/'.$this->getMarketplaceID();
|
return '/v1/marketplaces/'.$this->getMarketplaceID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,13 @@ final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider {
|
||||||
throw new Exception('!');
|
throw new Exception('!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function executeRefund(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
// TODO: Implement.
|
||||||
|
throw new PhortuneNotImplementedException($this);
|
||||||
|
}
|
||||||
|
|
||||||
private function getPaypalAPIUsername() {
|
private function getPaypalAPIUsername() {
|
||||||
return $this
|
return $this
|
||||||
->getProviderConfig()
|
->getProviderConfig()
|
||||||
|
|
|
@ -137,10 +137,20 @@ abstract class PhortunePaymentProvider {
|
||||||
$this->executeCharge($payment_method, $charge);
|
$this->executeCharge($payment_method, $charge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final public function refundCharge(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
$this->executeRefund($charge, $refund);
|
||||||
|
}
|
||||||
|
|
||||||
abstract protected function executeCharge(
|
abstract protected function executeCharge(
|
||||||
PhortunePaymentMethod $payment_method,
|
PhortunePaymentMethod $payment_method,
|
||||||
PhortuneCharge $charge);
|
PhortuneCharge $charge);
|
||||||
|
|
||||||
|
abstract protected function executeRefund(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $charge);
|
||||||
|
|
||||||
|
|
||||||
/* -( Adding Payment Methods )--------------------------------------------- */
|
/* -( Adding Payment Methods )--------------------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -146,12 +146,7 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
|
||||||
'capture' => true,
|
'capture' => true,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
|
||||||
$stripe_charge = Stripe_Charge::create($params, $secret_key);
|
$stripe_charge = Stripe_Charge::create($params, $secret_key);
|
||||||
} catch (Stripe_CardError $ex) {
|
|
||||||
// TODO: Fail charge explicitly.
|
|
||||||
throw $ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = $stripe_charge->id;
|
$id = $stripe_charge->id;
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
|
@ -162,6 +157,41 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider {
|
||||||
$charge->save();
|
$charge->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function executeRefund(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
|
||||||
|
$charge_id = $charge->getMetadataValue('stripe.chargeID');
|
||||||
|
if (!$charge_id) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Unable to refund charge; no Stripe chargeID!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = dirname(phutil_get_library_root('phabricator'));
|
||||||
|
require_once $root.'/externals/stripe-php/lib/Stripe.php';
|
||||||
|
|
||||||
|
$refund_cents = $refund
|
||||||
|
->getAmountAsCurrency()
|
||||||
|
->negate()
|
||||||
|
->getValueInUSDCents();
|
||||||
|
|
||||||
|
$secret_key = $this->getSecretKey();
|
||||||
|
$params = array(
|
||||||
|
'amount' => $refund_cents,
|
||||||
|
);
|
||||||
|
|
||||||
|
$stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);
|
||||||
|
$stripe_refund = $stripe_charge->refunds->create($params);
|
||||||
|
|
||||||
|
$id = $stripe_refund->id;
|
||||||
|
if (!$id) {
|
||||||
|
throw new Exception(pht('Stripe refund call did not return an ID!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$charge->setMetadataValue('stripe.refundID', $id);
|
||||||
|
$charge->save();
|
||||||
|
}
|
||||||
|
|
||||||
private function getPublishableKey() {
|
private function getPublishableKey() {
|
||||||
return $this
|
return $this
|
||||||
->getProviderConfig()
|
->getProviderConfig()
|
||||||
|
|
|
@ -56,6 +56,12 @@ final class PhortuneTestPaymentProvider extends PhortunePaymentProvider {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function executeRefund(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
public function getAllConfigurableProperties() {
|
public function getAllConfigurableProperties() {
|
||||||
return array();
|
return array();
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,6 +186,29 @@ final class PhortuneWePayPaymentProvider extends PhortunePaymentProvider {
|
||||||
->getMetadataValue(self::WEPAY_ACCOUNT_ID);
|
->getMetadataValue(self::WEPAY_ACCOUNT_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function executeRefund(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
|
||||||
|
$root = dirname(phutil_get_library_root('phabricator'));
|
||||||
|
require_once $root.'/externals/wepay/wepay.php';
|
||||||
|
|
||||||
|
WePay::useStaging(
|
||||||
|
$this->getWePayClientID(),
|
||||||
|
$this->getWePayClientSecret());
|
||||||
|
|
||||||
|
$wepay = new WePay($this->getWePayAccessToken());
|
||||||
|
|
||||||
|
$charge_id = $charge->getMetadataValue('wepay.checkoutID');
|
||||||
|
|
||||||
|
$params = array(
|
||||||
|
'checkout_id' => $charge_id,
|
||||||
|
'refund_reason' => pht('Refund'),
|
||||||
|
'amount' => $refund->getAmountAsCurrency()->negate()->formatBareValue(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$wepay->request('checkout/refund', $params);
|
||||||
|
}
|
||||||
|
|
||||||
/* -( One-Time Payments )-------------------------------------------------- */
|
/* -( One-Time Payments )-------------------------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,13 @@ final class PhortuneAccount extends PhortuneDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPolicy($capability) {
|
public function getPolicy($capability) {
|
||||||
|
switch ($capability) {
|
||||||
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||||
|
// Accounts are technically visible to all users, because merchant
|
||||||
|
// controllers need to be able to see accounts in order to process
|
||||||
|
// orders. We lock things down more tightly at the application level.
|
||||||
|
return PhabricatorPolicies::POLICY_USER;
|
||||||
|
case PhabricatorPolicyCapability::CAN_EDIT:
|
||||||
if ($this->getPHID() === null) {
|
if ($this->getPHID() === null) {
|
||||||
// Allow a user to create an account for themselves.
|
// Allow a user to create an account for themselves.
|
||||||
return PhabricatorPolicies::POLICY_USER;
|
return PhabricatorPolicies::POLICY_USER;
|
||||||
|
@ -113,6 +120,7 @@ final class PhortuneAccount extends PhortuneDAO
|
||||||
return PhabricatorPolicies::POLICY_NOONE;
|
return PhabricatorPolicies::POLICY_NOONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
||||||
$members = array_fuse($this->getMemberPHIDs());
|
$members = array_fuse($this->getMemberPHIDs());
|
||||||
|
|
|
@ -98,14 +98,16 @@ final class PhortuneCart extends PhortuneDAO
|
||||||
if ($copy->getStatus() !== self::STATUS_READY) {
|
if ($copy->getStatus() !== self::STATUS_READY) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
pht(
|
pht(
|
||||||
'Cart has wrong status ("%s") to call willApplyCharge(), expected '.
|
'Cart has wrong status ("%s") to call willApplyCharge(), '.
|
||||||
'"%s".',
|
'expected "%s".',
|
||||||
$copy->getStatus(),
|
$copy->getStatus(),
|
||||||
self::STATUS_READY));
|
self::STATUS_READY));
|
||||||
}
|
}
|
||||||
|
|
||||||
$charge->save();
|
$charge->save();
|
||||||
$this->setStatus(PhortuneCart::STATUS_PURCHASING)->save();
|
$this->setStatus(PhortuneCart::STATUS_PURCHASING)->save();
|
||||||
|
|
||||||
|
$this->endReadLocking();
|
||||||
$this->saveTransaction();
|
$this->saveTransaction();
|
||||||
|
|
||||||
return $charge;
|
return $charge;
|
||||||
|
@ -123,14 +125,16 @@ final class PhortuneCart extends PhortuneDAO
|
||||||
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
|
if ($copy->getStatus() !== self::STATUS_PURCHASING) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
pht(
|
pht(
|
||||||
'Cart has wrong status ("%s") to call didApplyCharge(), expected '.
|
'Cart has wrong status ("%s") to call didApplyCharge(), '.
|
||||||
'"%s".',
|
'expected "%s".',
|
||||||
$copy->getStatus(),
|
$copy->getStatus(),
|
||||||
self::STATUS_PURCHASING));
|
self::STATUS_PURCHASING));
|
||||||
}
|
}
|
||||||
|
|
||||||
$charge->save();
|
$charge->save();
|
||||||
$this->setStatus(self::STATUS_CHARGED)->save();
|
$this->setStatus(self::STATUS_CHARGED)->save();
|
||||||
|
|
||||||
|
$this->endReadLocking();
|
||||||
$this->saveTransaction();
|
$this->saveTransaction();
|
||||||
|
|
||||||
foreach ($this->purchases as $purchase) {
|
foreach ($this->purchases as $purchase) {
|
||||||
|
@ -142,6 +146,127 @@ final class PhortuneCart extends PhortuneDAO
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function willRefundCharge(
|
||||||
|
PhabricatorUser $actor,
|
||||||
|
PhortunePaymentProvider $provider,
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCurrency $amount) {
|
||||||
|
|
||||||
|
if (!$amount->isPositive()) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Trying to refund nonpositive amount of money!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Trying to refund more money than remaining on charge!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($charge->getRefundedChargePHID()) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Trying to refund a refund!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Trying to refund an uncharged charge!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$refund_charge = PhortuneCharge::initializeNewCharge()
|
||||||
|
->setAccountPHID($this->getAccount()->getPHID())
|
||||||
|
->setCartPHID($this->getPHID())
|
||||||
|
->setAuthorPHID($actor->getPHID())
|
||||||
|
->setMerchantPHID($this->getMerchant()->getPHID())
|
||||||
|
->setProviderPHID($provider->getProviderConfig()->getPHID())
|
||||||
|
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
|
||||||
|
->setRefundedChargePHID($charge->getPHID())
|
||||||
|
->setAmountAsCurrency($amount->negate());
|
||||||
|
|
||||||
|
$charge->openTransaction();
|
||||||
|
$charge->beginReadLocking();
|
||||||
|
|
||||||
|
$copy = clone $charge;
|
||||||
|
$copy->reload();
|
||||||
|
|
||||||
|
if ($copy->getRefundingPHID() !== null) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Trying to refund a charge which is already refunding!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$refund_charge->save();
|
||||||
|
$charge->setRefundingPHID($refund_charge->getPHID());
|
||||||
|
$charge->save();
|
||||||
|
|
||||||
|
$charge->endReadLocking();
|
||||||
|
$charge->saveTransaction();
|
||||||
|
|
||||||
|
return $refund_charge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function didRefundCharge(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
|
||||||
|
$refund->setStatus(PhortuneCharge::STATUS_CHARGED);
|
||||||
|
|
||||||
|
$this->openTransaction();
|
||||||
|
$this->beginReadLocking();
|
||||||
|
|
||||||
|
$copy = clone $charge;
|
||||||
|
$copy->reload();
|
||||||
|
|
||||||
|
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Charge is in the wrong refunding state!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$charge->setRefundingPHID(null);
|
||||||
|
|
||||||
|
// NOTE: There's some trickiness here to get the signs right. Both
|
||||||
|
// these values are positive but the refund has a negative value.
|
||||||
|
$total_refunded = $charge
|
||||||
|
->getAmountRefundedAsCurrency()
|
||||||
|
->add($refund->getAmountAsCurrency()->negate());
|
||||||
|
|
||||||
|
$charge->setAmountRefundedAsCurrency($total_refunded);
|
||||||
|
$charge->save();
|
||||||
|
$refund->save();
|
||||||
|
|
||||||
|
$this->endReadLocking();
|
||||||
|
$this->saveTransaction();
|
||||||
|
|
||||||
|
foreach ($this->purchases as $purchase) {
|
||||||
|
$purchase->getProduct()->didRefundProduct($purchase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function didFailRefund(
|
||||||
|
PhortuneCharge $charge,
|
||||||
|
PhortuneCharge $refund) {
|
||||||
|
|
||||||
|
$refund->setStatus(PhortuneCharge::STATUS_FAILED);
|
||||||
|
|
||||||
|
$this->openTransaction();
|
||||||
|
$this->beginReadLocking();
|
||||||
|
|
||||||
|
$copy = clone $charge;
|
||||||
|
$copy->reload();
|
||||||
|
|
||||||
|
if ($charge->getRefundingPHID() !== $refund->getPHID()) {
|
||||||
|
throw new Exception(
|
||||||
|
pht('Charge is in the wrong refunding state!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$charge->setRefundingPHID(null);
|
||||||
|
$charge->save();
|
||||||
|
$refund->save();
|
||||||
|
|
||||||
|
$this->endReadLocking();
|
||||||
|
$this->saveTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
public function getName() {
|
public function getName() {
|
||||||
return $this->getImplementation()->getName($this);
|
return $this->getImplementation()->getName($this);
|
||||||
}
|
}
|
||||||
|
@ -162,6 +287,56 @@ final class PhortuneCart extends PhortuneDAO
|
||||||
return '/phortune/cart/'.$this->getID().'/checkout/';
|
return '/phortune/cart/'.$this->getID().'/checkout/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canCancelOrder() {
|
||||||
|
try {
|
||||||
|
$this->assertCanCancelOrder();
|
||||||
|
return true;
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canRefundOrder() {
|
||||||
|
try {
|
||||||
|
$this->assertCanRefundOrder();
|
||||||
|
return true;
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assertCanCancelOrder() {
|
||||||
|
switch ($this->getStatus()) {
|
||||||
|
case self::STATUS_BUILDING:
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'This order can not be cancelled because the application has not '.
|
||||||
|
'finished building it yet.'));
|
||||||
|
case self::STATUS_READY:
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'This order can not be cancelled because it has not been placed.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getImplementation()->assertCanCancelOrder($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assertCanRefundOrder() {
|
||||||
|
switch ($this->getStatus()) {
|
||||||
|
case self::STATUS_BUILDING:
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'This order can not be refunded because the application has not '.
|
||||||
|
'finished building it yet.'));
|
||||||
|
case self::STATUS_READY:
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'This order can not be refunded because it has not been placed.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->getImplementation()->assertCanRefundOrder($this);
|
||||||
|
}
|
||||||
|
|
||||||
public function getConfiguration() {
|
public function getConfiguration() {
|
||||||
return array(
|
return array(
|
||||||
self::CONFIG_AUX_PHID => true,
|
self::CONFIG_AUX_PHID => true,
|
||||||
|
@ -260,11 +435,30 @@ final class PhortuneCart extends PhortuneDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
||||||
return $this->getAccount()->hasAutomaticCapability($capability, $viewer);
|
if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the viewer controls the merchant this order was placed with, they
|
||||||
|
// can view the order.
|
||||||
|
if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
|
||||||
|
$can_admin = PhabricatorPolicyFilter::hasCapability(
|
||||||
|
$viewer,
|
||||||
|
$this->getMerchant(),
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
if ($can_admin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAutomaticCapability($capability) {
|
public function describeAutomaticCapability($capability) {
|
||||||
return pht('Carts inherit the policies of the associated account.');
|
return array(
|
||||||
|
pht('Orders inherit the policies of the associated account.'),
|
||||||
|
pht('The merchant you placed an order with can review and manage it.'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,9 @@ final class PhortuneCharge extends PhortuneDAO
|
||||||
protected $merchantPHID;
|
protected $merchantPHID;
|
||||||
protected $paymentMethodPHID;
|
protected $paymentMethodPHID;
|
||||||
protected $amountAsCurrency;
|
protected $amountAsCurrency;
|
||||||
|
protected $amountRefundedAsCurrency;
|
||||||
|
protected $refundedChargePHID;
|
||||||
|
protected $refundingPHID;
|
||||||
protected $status;
|
protected $status;
|
||||||
protected $metadata = array();
|
protected $metadata = array();
|
||||||
|
|
||||||
|
@ -28,7 +31,8 @@ final class PhortuneCharge extends PhortuneDAO
|
||||||
|
|
||||||
public static function initializeNewCharge() {
|
public static function initializeNewCharge() {
|
||||||
return id(new PhortuneCharge())
|
return id(new PhortuneCharge())
|
||||||
->setStatus(self::STATUS_CHARGING);
|
->setStatus(self::STATUS_CHARGING)
|
||||||
|
->setAmountRefundedAsCurrency(PhortuneCurrency::newEmptyCurrency());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getConfiguration() {
|
public function getConfiguration() {
|
||||||
|
@ -39,11 +43,14 @@ final class PhortuneCharge extends PhortuneDAO
|
||||||
),
|
),
|
||||||
self::CONFIG_APPLICATION_SERIALIZERS => array(
|
self::CONFIG_APPLICATION_SERIALIZERS => array(
|
||||||
'amountAsCurrency' => new PhortuneCurrencySerializer(),
|
'amountAsCurrency' => new PhortuneCurrencySerializer(),
|
||||||
|
'amountRefundedAsCurrency' => new PhortuneCurrencySerializer(),
|
||||||
),
|
),
|
||||||
self::CONFIG_COLUMN_SCHEMA => array(
|
self::CONFIG_COLUMN_SCHEMA => array(
|
||||||
'paymentProviderKey' => 'text128',
|
|
||||||
'paymentMethodPHID' => 'phid?',
|
'paymentMethodPHID' => 'phid?',
|
||||||
|
'refundedChargePHID' => 'phid?',
|
||||||
|
'refundingPHID' => 'phid?',
|
||||||
'amountAsCurrency' => 'text64',
|
'amountAsCurrency' => 'text64',
|
||||||
|
'amountRefundedAsCurrency' => 'text64',
|
||||||
'status' => 'text32',
|
'status' => 'text32',
|
||||||
),
|
),
|
||||||
self::CONFIG_KEY_SCHEMA => array(
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
|
@ -75,6 +82,26 @@ final class PhortuneCharge extends PhortuneDAO
|
||||||
return idx(self::getStatusNameMap(), $status, pht('Unknown'));
|
return idx(self::getStatusNameMap(), $status, pht('Unknown'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getStatusForDisplay() {
|
||||||
|
if ($this->getStatus() == self::STATUS_CHARGED) {
|
||||||
|
if ($this->getRefundedChargePHID()) {
|
||||||
|
return pht('Refund');
|
||||||
|
}
|
||||||
|
|
||||||
|
$refunded = $this->getAmountRefundedAsCurrency();
|
||||||
|
|
||||||
|
if ($refunded->isPositive()) {
|
||||||
|
if ($refunded->isEqualTo($this->getAmountAsCurrency())) {
|
||||||
|
return pht('Fully Refunded');
|
||||||
|
} else {
|
||||||
|
return pht('%s Refunded', $refunded->formatForDisplay());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::getNameForStatus($this->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
public function generatePHID() {
|
public function generatePHID() {
|
||||||
return PhabricatorPHID::generateNewPHID(
|
return PhabricatorPHID::generateNewPHID(
|
||||||
PhortuneChargePHIDType::TYPECONST);
|
PhortuneChargePHIDType::TYPECONST);
|
||||||
|
@ -107,6 +134,21 @@ final class PhortuneCharge extends PhortuneDAO
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAmountRefundableAsCurrency() {
|
||||||
|
$amount = $this->getAmountAsCurrency();
|
||||||
|
$refunded = $this->getAmountRefundedAsCurrency();
|
||||||
|
|
||||||
|
// We can't refund negative amounts of money, since it does not make
|
||||||
|
// sense and is not possible in the various payment APIs.
|
||||||
|
|
||||||
|
$refundable = $amount->subtract($refunded);
|
||||||
|
if ($refundable->isPositive()) {
|
||||||
|
return $refundable;
|
||||||
|
} else {
|
||||||
|
return PhortuneCurrency::newEmptyCurrency();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,10 @@ final class PhortuneProduct extends PhortuneDAO
|
||||||
return $this->getImplementation()->didPurchaseProduct($this, $purchase);
|
return $this->getImplementation()->didPurchaseProduct($this, $purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function didRefundProduct(PhortunePurchase $purchase) {
|
||||||
|
return $this->getImplementation()->didRefundProduct($this, $purchase);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue