Page MenuHomePhabricator

D10667.id25624.diff
No OneTemporary

D10667.id25624.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2563,6 +2563,7 @@
'PhortuneCartPHIDType' => 'applications/phortune/phid/PhortuneCartPHIDType.php',
'PhortuneCartQuery' => 'applications/phortune/query/PhortuneCartQuery.php',
'PhortuneCartSearchEngine' => 'applications/phortune/query/PhortuneCartSearchEngine.php',
+ 'PhortuneCartUpdateController' => 'applications/phortune/controller/PhortuneCartUpdateController.php',
'PhortuneCartViewController' => 'applications/phortune/controller/PhortuneCartViewController.php',
'PhortuneCharge' => 'applications/phortune/storage/PhortuneCharge.php',
'PhortuneChargePHIDType' => 'applications/phortune/phid/PhortuneChargePHIDType.php',
@@ -5618,6 +5619,7 @@
'PhortuneCartPHIDType' => 'PhabricatorPHIDType',
'PhortuneCartQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneCartSearchEngine' => 'PhabricatorApplicationSearchEngine',
+ 'PhortuneCartUpdateController' => 'PhortuneCartController',
'PhortuneCartViewController' => 'PhortuneCartController',
'PhortuneCharge' => array(
'PhortuneDAO',
diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
--- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php
+++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php
@@ -49,6 +49,7 @@
'' => 'PhortuneCartViewController',
'checkout/' => 'PhortuneCartCheckoutController',
'(?P<action>cancel|refund)/' => 'PhortuneCartCancelController',
+ 'update/' => 'PhortuneCartUpdateController',
),
'account/' => array(
'' => 'PhortuneAccountListController',
diff --git a/src/applications/phortune/controller/PhortuneCartCancelController.php b/src/applications/phortune/controller/PhortuneCartCancelController.php
--- a/src/applications/phortune/controller/PhortuneCartCancelController.php
+++ b/src/applications/phortune/controller/PhortuneCartCancelController.php
@@ -68,6 +68,7 @@
->withCartPHIDs(array($cart->getPHID()))
->withStatuses(
array(
+ PhortuneCharge::STATUS_HOLD,
PhortuneCharge::STATUS_CHARGED,
))
->execute();
@@ -156,6 +157,10 @@
throw new Exception(pht('Unable to refund some charges!'));
}
+ // TODO: If every HOLD and CHARGING transaction has been fully refunded
+ // and we're in a HOLD, PURCHASING or CHARGED cart state we probably
+ // need to kick the cart back to READY here?
+
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
}
@@ -182,6 +187,10 @@
'Really cancel this order? Any payment will be refunded.');
$button = pht('Cancel Order');
+ // Don't give the user a "Cancel" button in response to a "Cancel?"
+ // prompt, as it's confusing.
+ $cancel_text = pht('Do Not Cancel Order');
+
$form = null;
}
@@ -190,6 +199,6 @@
->appendChild($body)
->appendChild($form)
->addSubmitButton($button)
- ->addCancelButton($cancel_uri);
+ ->addCancelButton($cancel_uri, $cancel_text);
}
}
diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php
--- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php
+++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php
@@ -39,37 +39,12 @@
// 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_PURCHASING:
+ case PhortuneCart::STATUS_HOLD:
case PhortuneCart::STATUS_PURCHASED:
+ // For these states, kick the user to the order page to give them
+ // information and options.
return id(new AphrontRedirectResponse())->setURI($cart->getDetailURI());
default:
throw new Exception(
diff --git a/src/applications/phortune/controller/PhortuneCartUpdateController.php b/src/applications/phortune/controller/PhortuneCartUpdateController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/phortune/controller/PhortuneCartUpdateController.php
@@ -0,0 +1,31 @@
+<?php
+
+final class PhortuneCartUpdateController
+ 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();
+ }
+
+ // TODO: This obviously doesn't do anything for now.
+
+ return id(new AphrontRedirectResponse())
+ ->setURI($cart->getDetailURI());
+ }
+
+}
diff --git a/src/applications/phortune/controller/PhortuneCartViewController.php b/src/applications/phortune/controller/PhortuneCartViewController.php
--- a/src/applications/phortune/controller/PhortuneCartViewController.php
+++ b/src/applications/phortune/controller/PhortuneCartViewController.php
@@ -29,8 +29,56 @@
$cart_table = $this->buildCartContentTable($cart);
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $cart,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $errors = array();
+ $resume_uri = null;
+ switch ($cart->getStatus()) {
+ case PhortuneCart::STATUS_PURCHASING:
+ if ($can_edit) {
+ $resume_uri = $cart->getMetadataValue('provider.checkoutURI');
+ if ($resume_uri) {
+ $errors[] = pht(
+ 'The checkout process has been started, but not yet completed. '.
+ 'You can continue checking out by clicking %s, or cancel the '.
+ 'order, or contact the merchant for assistance.',
+ phutil_tag('strong', array(), pht('Continue Checkout')));
+ } else {
+ $errors[] = pht(
+ 'The checkout process has been started, but an error occurred. '.
+ 'You can cancel the order or contact the merchant for '.
+ 'assistance.');
+ }
+ }
+ break;
+ case PhortuneCart::STATUS_CHARGED:
+ if ($can_edit) {
+ $errors[] = pht(
+ 'You have been charged, but processing could not be completed. '.
+ 'You can cancel your order, or contact the merchant for '.
+ 'assistance.');
+ }
+ break;
+ case PhortuneCart::STATUS_HOLD:
+ if ($can_edit) {
+ $errors[] = pht(
+ 'Payment for this order is on hold. You can click %s to check '.
+ 'for updates, cancel the order, or contact the merchant for '.
+ 'assistance.',
+ phutil_tag('strong', array(), pht('Update Status')));
+ }
+ break;
+ }
+
$properties = $this->buildPropertyListView($cart);
- $actions = $this->buildActionListView($cart, $can_admin);
+ $actions = $this->buildActionListView(
+ $cart,
+ $can_edit,
+ $can_admin,
+ $resume_uri);
$properties->setActionList($actions);
$header = id(new PHUIHeaderView())
@@ -40,6 +88,7 @@
$cart_box = id(new PHUIObjectBoxView())
->setHeader($header)
+ ->setFormErrors($errors)
->appendChild($properties)
->appendChild($cart_table);
@@ -106,15 +155,15 @@
return $view;
}
- private function buildActionListView(PhortuneCart $cart, $can_admin) {
+ private function buildActionListView(
+ PhortuneCart $cart,
+ $can_edit,
+ $can_admin,
+ $resume_uri) {
+
$viewer = $this->getRequest()->getUser();
$id = $cart->getID();
- $can_edit = PhabricatorPolicyFilter::hasCapability(
- $viewer,
- $cart,
- PhabricatorPolicyCapability::CAN_EDIT);
-
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($cart);
@@ -123,6 +172,7 @@
$cancel_uri = $this->getApplicationURI("cart/{$id}/cancel/");
$refund_uri = $this->getApplicationURI("cart/{$id}/refund/");
+ $update_uri = $this->getApplicationURI("cart/{$id}/update/");
$view->addAction(
id(new PhabricatorActionView())
@@ -141,6 +191,20 @@
->setHref($refund_uri));
}
+ $view->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Update Status'))
+ ->setIcon('fa-refresh')
+ ->setHref($update_uri));
+
+ if ($can_edit && $resume_uri) {
+ $view->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Continue Checkout'))
+ ->setIcon('fa-shopping-cart')
+ ->setHref($resume_uri));
+ }
+
return $view;
}
diff --git a/src/applications/phortune/controller/PhortuneProviderActionController.php b/src/applications/phortune/controller/PhortuneProviderActionController.php
--- a/src/applications/phortune/controller/PhortuneProviderActionController.php
+++ b/src/applications/phortune/controller/PhortuneProviderActionController.php
@@ -1,6 +1,7 @@
<?php
-final class PhortuneProviderActionController extends PhortuneController {
+final class PhortuneProviderActionController
+ extends PhortuneController {
private $id;
private $action;
diff --git a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
--- a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
+++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php
@@ -7,11 +7,6 @@
const PAYPAL_API_SIGNATURE = 'paypal.api-signature';
const PAYPAL_MODE = 'paypal.mode';
- public function isEnabled() {
- // TODO: See note in processControllerRequest().
- return false;
- }
-
public function isAcceptingLivePayments() {
$mode = $this->getProviderConfig()->getMetadataValue(self::PAYPAL_MODE);
return ($mode === 'live');
@@ -170,8 +165,31 @@
protected function executeRefund(
PhortuneCharge $charge,
PhortuneCharge $refund) {
- // TODO: Implement.
- throw new PhortuneNotImplementedException($this);
+
+ $transaction_id = $charge->getMetadataValue('paypal.transactionID');
+ if (!$transaction_id) {
+ throw new Exception(pht('Charge has no transaction ID!'));
+ }
+
+ $refund_amount = $refund->getAmountAsCurrency()->negate();
+ $refund_currency = $refund_amount->getCurrency();
+ $refund_value = $refund_amount->formatBareValue();
+
+ $params = array(
+ 'TRANSACTIONID' => $transaction_id,
+ 'REFUNDTYPE' => 'Partial',
+ 'AMT' => $refund_value,
+ 'CURRENCYCODE' => $refund_currency,
+ );
+
+ $result = $this
+ ->newPaypalAPICall()
+ ->setRawPayPalQuery('RefundTransaction', $params)
+ ->resolve();
+
+ $charge->setMetadataValue(
+ 'paypal.refundID',
+ $result['REFUNDTRANSACTIONID']);
}
private function getPaypalAPIUsername() {
@@ -281,7 +299,7 @@
'token' => $result['TOKEN'],
));
- $cart->setMetadataValue('provider.checkoutURI', $uri);
+ $cart->setMetadataValue('provider.checkoutURI', (string)$uri);
$cart->save();
$charge->setMetadataValue('paypal.token', $result['TOKEN']);
@@ -291,6 +309,11 @@
->setIsExternal(true)
->setURI($uri);
case 'charge':
+ if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
+ return id(new AphrontRedirectResponse())
+ ->setURI($cart->getCheckoutURI());
+ }
+
$token = $request->getStr('token');
$params = array(
@@ -302,19 +325,19 @@
->setRawPayPalQuery('GetExpressCheckoutDetails', $params)
->resolve();
- var_dump($result);
-
if ($result['CUSTOM'] !== $charge->getPHID()) {
throw new Exception(
pht('Paypal checkout does not match Phortune charge!'));
}
if ($result['CHECKOUTSTATUS'] !== 'PaymentActionNotInitiated') {
- throw new Exception(
- pht(
- 'Expected status "%s", got "%s".',
- 'PaymentActionNotInitiated',
- $result['CHECKOUTSTATUS']));
+ return $controller->newDialog()
+ ->setTitle(pht('Payment Already Processed'))
+ ->appendParagraph(
+ pht(
+ 'The payment response for this charge attempt has already '.
+ 'been processed.'))
+ ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
}
$price = $cart->getTotalPriceAsCurrency();
@@ -333,22 +356,53 @@
->setRawPayPalQuery('DoExpressCheckoutPayment', $params)
->resolve();
- // TODO: Paypal can send requests back in "PaymentReview" status,
- // and does this for test transactions. We're supposed to hold
- // the transaction and poll the API every 6 hours. This is unreasonably
- // difficult for now and we can't reasonably just fail these charges.
+ $transaction_id = $result['PAYMENTINFO_0_TRANSACTIONID'];
+
+ $success = false;
+ $hold = false;
+ switch ($result['PAYMENTINFO_0_PAYMENTSTATUS']) {
+ case 'Processed':
+ case 'Completed':
+ case 'Completed-Funds-Held':
+ $success = true;
+ break;
+ case 'In-Progress':
+ case 'Pending':
+ // TODO: We can capture more information about this stuff.
+ $hold = true;
+ break;
+ case 'Denied':
+ case 'Expired':
+ case 'Failed':
+ case 'Partially-Refunded':
+ case 'Canceled-Reversal':
+ case 'None':
+ case 'Refunded':
+ case 'Reversed':
+ case 'Voided':
+ default:
+ // These are all failure states.
+ break;
+ }
- var_dump($result);
- die();
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- $success = false; // TODO: <----
+ $charge->setMetadataValue('paypal.transactionID', $transaction_id);
+ $charge->save();
- // TODO: Clean this up once that mess up there ^^^^^ gets cleaned up.
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($success) {
$cart->didApplyCharge($charge);
$response = id(new AphrontRedirectResponse())->setURI(
- $cart->getDoneURI());
+ $cart->getDoneURI());
+ } else if ($hold) {
+ $cart->didHoldCharge($charge);
+
+ $response = $controller
+ ->newDialog()
+ ->setTitle(pht('Charge On Hold'))
+ ->appendParagraph(
+ pht('Your charge is on hold, for reasons?'))
+ ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
} else {
$cart->didFailCharge($charge);
@@ -361,8 +415,16 @@
return $response;
case 'cancel':
- var_dump($_REQUEST);
- break;
+ if ($cart->getStatus() !== PhortuneCart::STATUS_PURCHASING) {
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ // TODO: Since the user cancelled this, we could conceivably just
+ // throw it away or make it more clear that it's a user cancel.
+ $cart->didFailCharge($charge);
+ unset($unguarded);
+ }
+
+ return id(new AphrontRedirectResponse())
+ ->setURI($cart->getCheckoutURI());
}
throw new Exception(
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -7,6 +7,7 @@
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
+ const STATUS_HOLD = 'cart:hold';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
@@ -57,6 +58,7 @@
self::STATUS_READY => pht('Ready'),
self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'),
+ self::STATUS_HOLD => pht('Hold'),
self::STATUS_PURCHASED => pht('Purchased'),
);
}
@@ -113,6 +115,31 @@
return $charge;
}
+ public function didHoldCharge(PhortuneCharge $charge) {
+ $charge->setStatus(PhortuneCharge::STATUS_HOLD);
+
+ $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 didHoldCharge(), '.
+ 'expected "%s".',
+ $copy->getStatus(),
+ self::STATUS_PURCHASING));
+ }
+
+ $charge->save();
+ $this->setStatus(self::STATUS_HOLD)->save();
+
+ $this->endReadLocking();
+ $this->saveTransaction();
+ }
+
public function didApplyCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_CHARGED);
@@ -198,7 +225,8 @@
pht('Trying to refund a refund!'));
}
- if ($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) {
+ if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
+ ($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
throw new Exception(
pht('Trying to refund an uncharged charge!'));
}
@@ -462,7 +490,11 @@
}
public function getPolicy($capability) {
- return $this->getAccount()->getPolicy($capability);
+ // NOTE: Both view and edit use the account's edit policy. We punch a hole
+ // through this for merchants, below.
+ return $this
+ ->getAccount()
+ ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
diff --git a/src/applications/phortune/storage/PhortuneCharge.php b/src/applications/phortune/storage/PhortuneCharge.php
--- a/src/applications/phortune/storage/PhortuneCharge.php
+++ b/src/applications/phortune/storage/PhortuneCharge.php
@@ -11,6 +11,7 @@
const STATUS_CHARGING = 'charge:charging';
const STATUS_CHARGED = 'charge:charged';
+ const STATUS_HOLD = 'charge:hold';
const STATUS_FAILED = 'charge:failed';
protected $accountPHID;
@@ -74,6 +75,7 @@
return array(
self::STATUS_CHARGING => pht('Charging'),
self::STATUS_CHARGED => pht('Charged'),
+ self::STATUS_HOLD => pht('Hold'),
self::STATUS_FAILED => pht('Failed'),
);
}

File Metadata

Mime Type
text/plain
Expires
Mar 16 2025, 8:31 PM (5 w, 2 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7707690
Default Alt Text
D10667.id25624.diff (19 KB)

Event Timeline