diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php index 8f4cc73ea1..81b1d70b1a 100644 --- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php @@ -1,221 +1,224 @@ 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(); 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())) ->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 = id(new PhortuneCharge()) - ->setAccountPHID($account->getPHID()) - ->setCartPHID($cart->getPHID()) - ->setAuthorPHID($viewer->getPHID()) - ->setPaymentProviderKey($provider->getProviderKey()) - ->setPaymentMethodPHID($method->getPHID()) - ->setAmountAsCurrency($cart->getTotalPriceAsCurrency()) - ->setStatus(PhortuneCharge::STATUS_PENDING); - - $charge->openTransaction(); - $charge->save(); - - $cart->setStatus(PhortuneCart::STATUS_PURCHASING); - $cart->save(); - $charge->saveTransaction(); - + $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/'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild($method_control); $add_providers = PhortunePaymentProvider::getProvidersForAddPaymentMethod(); 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(); 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/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index 85cbdced3a..a0fa333b93 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -1,251 +1,258 @@ setAncestorClass('PhortunePaymentProvider') ->loadObjects(); return mpull($objects, null, 'getProviderKey'); } 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; } public static function getProviderByDigest($digest) { $providers = self::getEnabledProviders(); foreach ($providers as $key => $provider) { $provider_digest = PhabricatorHash::digestForIndex($key); if ($provider_digest == $digest) { return $provider; } } return null; } abstract public function isEnabled(); final public function getProviderKey() { return $this->getProviderType().'@'.$this->getProviderDomain(); } /** * Return a short string which uniquely identifies this provider's protocol * type, like "stripe", "paypal", or "balanced". */ abstract public function getProviderType(); /** * Return a short string which uniquely identifies the domain for this * provider, like "stripe.com" or "google.com". * * This is distinct from the provider type so that protocols are not bound * to a single domain. This is probably not relevant for payments, but this * assumption burned us pretty hard with authentication and it's easy enough * to avoid. */ abstract public function getProviderDomain(); abstract public function getPaymentMethodDescription(); abstract public function getPaymentMethodIcon(); abstract public function getPaymentMethodProviderDescription(); /** * Determine of a provider can handle a payment method. * * @return bool True if this provider can apply charges to the payment method. */ abstract public function canHandlePaymentMethod( PhortunePaymentMethod $method); final public function applyCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { $charge->setStatus(PhortuneCharge::STATUS_CHARGING); $charge->save(); $this->executeCharge($payment_method, $charge); $charge->setStatus(PhortuneCharge::STATUS_CHARGED); $charge->save(); } 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()) { + array $params = array(), + $local = false) { $digest = PhabricatorHash::digestForIndex($this->getProviderKey()); $app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication'); $path = $app->getBaseURI().'provider/'.$digest.'/'.$action.'/'; $uri = new PhutilURI($path); $uri->setQueryParams($params); - return PhabricatorEnv::getURI((string)$uri); + if ($local) { + return $uri; + } else { + return PhabricatorEnv::getURI((string)$uri); + } } public function canRespondToControllerAction($action) { return false; } public function processControllerRequest( PhortuneProviderController $controller, AphrontRequest $request) { throw new PhortuneNotImplementedException($this); } } diff --git a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php index b52dc87e5d..0c11e4186b 100644 --- a/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneWePayPaymentProvider.php @@ -1,213 +1,221 @@ getWePayClientID() && $this->getWePayClientSecret() && $this->getWePayAccessToken() && $this->getWePayAccountID(); } public function getProviderType() { return 'wepay'; } public function getProviderDomain() { return 'wepay.com'; } public function getPaymentMethodDescription() { return pht('Credit Card or Bank Account'); } public function getPaymentMethodIcon() { return celerity_get_resource_uri('/rsrc/image/phortune/wepay.png'); } public function getPaymentMethodProviderDescription() { return 'WePay'; } public function canHandlePaymentMethod(PhortunePaymentMethod $method) { $type = $method->getMetadataValue('type'); return ($type == 'wepay'); } protected function executeCharge( PhortunePaymentMethod $payment_method, PhortuneCharge $charge) { throw new Exception('!'); } private function getWePayClientID() { return PhabricatorEnv::getEnvConfig('phortune.wepay.client-id'); } private function getWePayClientSecret() { return PhabricatorEnv::getEnvConfig('phortune.wepay.client-secret'); } private function getWePayAccessToken() { return PhabricatorEnv::getEnvConfig('phortune.wepay.access-token'); } private function getWePayAccountID() { return PhabricatorEnv::getEnvConfig('phortune.wepay.account-id'); } /* -( One-Time Payments )-------------------------------------------------- */ public function canProcessOneTimePayments() { return true; } /* -( Controllers )-------------------------------------------------------- */ public function canRespondToControllerAction($action) { switch ($action) { case 'checkout': case 'charge': case 'cancel': return true; } return parent::canRespondToControllerAction(); } /** * @phutil-external-symbol class WePay */ public function processControllerRequest( PhortuneProviderController $controller, AphrontRequest $request) { $viewer = $request->getUser(); $cart = $controller->loadCart($request->getInt('cartID')); if (!$cart) { return new Aphront404Response(); } - $cart_uri = '/phortune/cart/'.$cart->getID().'/'; - $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(new PhortuneChargeQuery()) + ->setViewer($viewer) + ->withCartPHIDs(array($cart->getPHID())) + ->withStatuses( + array( + PhortuneCharge::STATUS_CHARGING, + )) + ->executeOne(); + + switch ($controller->getAction()) { + case 'checkout': + if ($charge) { + throw new Exception(pht('Cart is already charging!')); + } + break; + case 'charge': + case 'cancel': + if (!$charge) { + throw new Exception(pht('Cart is not charging yet!')); + } + break; + } + switch ($controller->getAction()) { case 'checkout': $return_uri = $this->getControllerURI( 'charge', array( 'cartID' => $cart->getID(), )); $cancel_uri = $this->getControllerURI( 'cancel', array( 'cartID' => $cart->getID(), )); $price = $cart->getTotalPriceAsCurrency(); $params = array( 'account_id' => $this->getWePayAccountID(), 'short_description' => 'Services', // TODO 'type' => 'SERVICE', 'amount' => $price->formatBareValue(), 'long_description' => 'Services', // TODO 'reference_id' => $cart->getPHID(), 'app_fee' => 0, 'fee_payer' => 'Payee', 'redirect_uri' => $return_uri, 'fallback_uri' => $cancel_uri, // NOTE: If we don't `auto_capture`, we might get a result back in // either an "authorized" or a "reserved" state. We can't capture // an "authorized" result, so just autocapture. 'auto_capture' => true, 'require_shipping' => 0, 'shipping_fee' => 0, 'charge_tax' => 0, 'mode' => 'regular', 'funding_sources' => 'bank,cc' ); + $cart->willApplyCharge($viewer, $this); + $result = $wepay->request('checkout/create', $params); - // TODO: We must store "$result->checkout_id" on the Cart since the - // user might not end up back here. Really this needs a bunch of junk. + $cart->setMetadataValue('provider.checkoutURI', $result->checkout_uri); + $cart->setMetadataValue('wepay.checkoutID', $result->checkout_id); + $cart->save(); $uri = new PhutilURI($result->checkout_uri); return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($uri); case 'charge': $checkout_id = $request->getInt('checkout_id'); $params = array( 'checkout_id' => $checkout_id, ); $checkout = $wepay->request('checkout', $params); if ($checkout->reference_id != $cart->getPHID()) { throw new Exception( pht('Checkout reference ID does not match cart PHID!')); } switch ($checkout->state) { case 'authorized': case 'reserved': case 'captured': break; default: throw new Exception( pht( 'Checkout is in bad state "%s"!', $result->state)); } - $currency = PhortuneCurrency::newFromString($checkout->gross, 'USD'); - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - $charge = id(new PhortuneCharge()) - ->setAmountAsCurrency($currency) - ->setAccountPHID($cart->getAccount()->getPHID()) - ->setAuthorPHID($viewer->getPHID()) - ->setPaymentProviderKey($this->getProviderKey()) - ->setCartPHID($cart->getPHID()) - ->setStatus(PhortuneCharge::STATUS_CHARGING) - ->save(); - - $cart->openTransaction(); - $charge->setStatus(PhortuneCharge::STATUS_CHARGED); - $charge->save(); - - $cart->setStatus(PhortuneCart::STATUS_PURCHASED); - $cart->save(); - $cart->saveTransaction(); - + $cart->didApplyCharge($charge); unset($unguarded); return id(new AphrontRedirectResponse()) - ->setIsExternal(true) - ->setURI($cart_uri); + ->setURI($cart->getDoneURI()); case 'cancel': - var_dump($_REQUEST); + // TODO: I don't know how it's possible to cancel out of a WePay + // charge workflow. + throw new Exception( + pht('How did you get here? WePay has no cancel flow in its UI...?')); break; } - throw new Exception("The rest of this isn't implemented yet."); + throw new Exception( + pht('Unsupported action "%s".', $controller->getAction())); } } diff --git a/src/applications/phortune/query/PhortuneChargeQuery.php b/src/applications/phortune/query/PhortuneChargeQuery.php index a706f7e7ed..aba655b4e4 100644 --- a/src/applications/phortune/query/PhortuneChargeQuery.php +++ b/src/applications/phortune/query/PhortuneChargeQuery.php @@ -1,131 +1,144 @@ 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 withCartPHIDs(array $cart_phids) { $this->cartPHIDs = $cart_phids; return $this; } + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + public function needCarts($need_carts) { $this->needCarts = $need_carts; return $this; } protected function loadPage() { $table = new PhortuneCharge(); $conn = $table->establishConnection('r'); $rows = queryfx_all( $conn, 'SELECT charge.* FROM %T charge %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn), $this->buildOrderClause($conn), $this->buildLimitClause($conn)); return $table->loadAllFromArray($rows); } protected function willFilterPage(array $charges) { $accounts = id(new PhortuneAccountQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($charges, 'getAccountPHID')) ->execute(); $accounts = mpull($accounts, null, 'getPHID'); foreach ($charges as $key => $charge) { $account = idx($accounts, $charge->getAccountPHID()); if (!$account) { unset($charges[$key]); continue; } $charge->attachAccount($account); } return $charges; } protected function didFilterPage(array $charges) { if ($this->needCarts) { $carts = id(new PhortuneCartQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs(mpull($charges, 'getCartPHID')) ->execute(); $carts = mpull($carts, null, 'getPHID'); foreach ($charges as $charge) { $cart = idx($carts, $charge->getCartPHID()); $charge->attachCart($cart); } } return $charges; } private function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); $where[] = $this->buildPagingClause($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'charge.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'charge.phid IN (%Ls)', $this->phids); } if ($this->accountPHIDs !== null) { $where[] = qsprintf( $conn, 'charge.accountPHID IN (%Ls)', $this->accountPHIDs); } if ($this->cartPHIDs !== null) { $where[] = qsprintf( $conn, 'charge.cartPHID IN (%Ls)', $this->cartPHIDs); } + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'charge.status IN (%Ls)', + $this->statuses); + } + return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorPhortuneApplication'; } } diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 6da806f949..36e1f8c2d7 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -1,186 +1,236 @@ setAuthorPHID($actor->getPHID()) ->setStatus(self::STATUS_BUILDING) ->setAccountPHID($account->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 didApplyCharge(PhortuneCharge $charge) { - if ($this->getStatus() !== self::STATUS_PURCHASING) { - throw new Exception( - pht( - 'Cart has wrong status ("%s") to call didApplyCharge(), expected '. - '"%s".', - $this->getStatus(), - self::STATUS_PURCHASING)); + 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()) + ->setAmountAsCurrency($this->getTotalPriceAsCurrency()); + + if ($method) { + $charge->setPaymentMethodPHID($method->getPHID()); } - $this->setStatus(self::STATUS_CHARGED)->save(); + $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'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_CART); } 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 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 c7d04e647b..1a33c36dd2 100644 --- a/src/applications/phortune/storage/PhortuneCharge.php +++ b/src/applications/phortune/storage/PhortuneCharge.php @@ -1,110 +1,113 @@ 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'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_CHRG); } 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.'); } }