diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3e980d3602..23712b6c87 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2817,6 +2817,8 @@ phutil_register_library_map(array( 'PhortuneAccountTransaction' => 'applications/phortune/storage/PhortuneAccountTransaction.php', 'PhortuneAccountTransactionQuery' => 'applications/phortune/query/PhortuneAccountTransactionQuery.php', 'PhortuneAccountViewController' => 'applications/phortune/controller/PhortuneAccountViewController.php', + 'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php', + 'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCartAcceptController' => 'applications/phortune/controller/PhortuneCartAcceptController.php', 'PhortuneCartCancelController' => 'applications/phortune/controller/PhortuneCartCancelController.php', @@ -2856,6 +2858,7 @@ phutil_register_library_map(array( 'PhortuneMerchantEditController' => 'applications/phortune/controller/PhortuneMerchantEditController.php', 'PhortuneMerchantEditor' => 'applications/phortune/editor/PhortuneMerchantEditor.php', 'PhortuneMerchantHasMemberEdgeType' => 'applications/phortune/edge/PhortuneMerchantHasMemberEdgeType.php', + 'PhortuneMerchantInvoiceCreateController' => 'applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php', 'PhortuneMerchantListController' => 'applications/phortune/controller/PhortuneMerchantListController.php', 'PhortuneMerchantPHIDType' => 'applications/phortune/phid/PhortuneMerchantPHIDType.php', 'PhortuneMerchantQuery' => 'applications/phortune/query/PhortuneMerchantQuery.php', @@ -6265,6 +6268,8 @@ phutil_register_library_map(array( 'PhortuneAccountTransaction' => 'PhabricatorApplicationTransaction', 'PhortuneAccountTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountViewController' => 'PhortuneController', + 'PhortuneAdHocCart' => 'PhortuneCartImplementation', + 'PhortuneAdHocProduct' => 'PhortuneProductImplementation', 'PhortuneCart' => array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', @@ -6312,6 +6317,7 @@ phutil_register_library_map(array( 'PhortuneMerchantEditController' => 'PhortuneMerchantController', 'PhortuneMerchantEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneMerchantHasMemberEdgeType' => 'PhabricatorEdgeType', + 'PhortuneMerchantInvoiceCreateController' => 'PhortuneMerchantController', 'PhortuneMerchantListController' => 'PhortuneMerchantController', 'PhortuneMerchantPHIDType' => 'PhabricatorPHIDType', 'PhortuneMerchantQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php index 2fab046648..cf7bc00b9a 100644 --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -84,19 +84,24 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication { 'edit/(?:(?P\d+)/)?' => 'PhortuneMerchantEditController', 'orders/(?P\d+)/(?:query/(?P[^/]+)/)?' => 'PhortuneCartListController', - '(?P\d+)/cart/(?P\d+)/' => array( - '' => 'PhortuneCartViewController', - '(?Pcancel|refund)/' => 'PhortuneCartCancelController', - 'update/' => 'PhortuneCartUpdateController', - 'accept/' => 'PhortuneCartAcceptController', - ), - '(?P\d+)/subscription/' => array( - '(?:query/(?P[^/]+)/)?' - => 'PhortuneSubscriptionListController', - 'view/(?P\d+)/' - => 'PhortuneSubscriptionViewController', - 'order/(?P\d+)/' - => 'PhortuneCartListController', + '(?P\d+)/' => array( + 'cart/(?P\d+)/' => array( + '' => 'PhortuneCartViewController', + '(?Pcancel|refund)/' => 'PhortuneCartCancelController', + 'update/' => 'PhortuneCartUpdateController', + 'accept/' => 'PhortuneCartAcceptController', + ), + 'subscription/' => array( + '(?:query/(?P[^/]+)/)?' + => 'PhortuneSubscriptionListController', + 'view/(?P\d+)/' + => 'PhortuneSubscriptionViewController', + 'order/(?P\d+)/' + => 'PhortuneCartListController', + ), + 'invoice/' => array( + 'new/' => 'PhortuneMerchantInvoiceCreateController', + ), ), '(?P\d+)/' => 'PhortuneMerchantViewController', ), diff --git a/src/applications/phortune/cart/PhortuneAdHocCart.php b/src/applications/phortune/cart/PhortuneAdHocCart.php new file mode 100644 index 0000000000..1d82c90062 --- /dev/null +++ b/src/applications/phortune/cart/PhortuneAdHocCart.php @@ -0,0 +1,39 @@ + $cart) { + $results[$key] = new PhortuneAdHocCart(); + } + + return $results; + } + + public function getName(PhortuneCart $cart) { + return $cart->getMetadataValue('adhoc.title'); + } + + public function getDescription(PhortuneCart $cart) { + return $cart->getMetadataValue('adhoc.description'); + } + + public function getCancelURI(PhortuneCart $cart) { + return null; + } + + public function getDoneURI(PhortuneCart $cart) { + return null; + } + + public function willCreateCart( + PhabricatorUser $viewer, + PhortuneCart $cart) { + return; + } + +} diff --git a/src/applications/phortune/cart/PhortuneCartImplementation.php b/src/applications/phortune/cart/PhortuneCartImplementation.php index 09c0187f49..9085fe9aad 100644 --- a/src/applications/phortune/cart/PhortuneCartImplementation.php +++ b/src/applications/phortune/cart/PhortuneCartImplementation.php @@ -16,6 +16,10 @@ abstract class PhortuneCartImplementation { abstract public function getCancelURI(PhortuneCart $cart); abstract public function getDoneURI(PhortuneCart $cart); + public function getDescription(PhortuneCart $cart) { + return null; + } + public function getDoneActionName(PhortuneCart $cart) { return pht('Return to Application'); } diff --git a/src/applications/phortune/controller/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/PhortuneCartCheckoutController.php index 6091dcd44b..b6b9287104 100644 --- a/src/applications/phortune/controller/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/PhortuneCartCheckoutController.php @@ -209,6 +209,8 @@ final class PhortuneCartCheckoutController ->appendChild($form) ->appendChild($provider_form); + $description_box = $this->renderCartDescription($cart); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Checkout')); $crumbs->addTextCrumb($title); @@ -217,6 +219,7 @@ final class PhortuneCartCheckoutController array( $crumbs, $cart_box, + $description_box, $payment_box, ), array( diff --git a/src/applications/phortune/controller/PhortuneCartController.php b/src/applications/phortune/controller/PhortuneCartController.php index 75269586b7..d3e19f52f1 100644 --- a/src/applications/phortune/controller/PhortuneCartController.php +++ b/src/applications/phortune/controller/PhortuneCartController.php @@ -42,4 +42,26 @@ abstract class PhortuneCartController return $table; } + protected function renderCartDescription(PhortuneCart $cart) { + $description = $cart->getDescription(); + if (!strlen($description)) { + return null; + } + + $output = PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff()) + ->setPreserveLinebreaks(true) + ->setContent($description), + 'default', + $this->getViewer()); + + $box = id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->appendChild($output); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Description')) + ->appendChild($box); + } + } diff --git a/src/applications/phortune/controller/PhortuneCartViewController.php b/src/applications/phortune/controller/PhortuneCartViewController.php index da17d3b706..a0bfd1779c 100644 --- a/src/applications/phortune/controller/PhortuneCartViewController.php +++ b/src/applications/phortune/controller/PhortuneCartViewController.php @@ -15,11 +15,16 @@ final class PhortuneCartViewController $authority = $this->loadMerchantAuthority(); - $cart = id(new PhortuneCartQuery()) + $query = id(new PhortuneCartQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) - ->needPurchases(true) - ->executeOne(); + ->needPurchases(true); + + if ($authority) { + $query->withMerchantPHIDs(array($authority->getPHID())); + } + + $cart = $query->executeOne(); if (!$cart) { return new Aphront404Response(); } @@ -35,6 +40,31 @@ final class PhortuneCartViewController $error_view = null; $resume_uri = null; switch ($cart->getStatus()) { + case PhortuneCart::STATUS_READY: + if ($authority && $request->getStr('invoice')) { + // We arrived here by following the ad-hoc invoice workflow, and + // are acting with merchant authority. + + $checkout_uri = PhabricatorEnv::getURI($cart->getCheckoutURI()); + + $invoice_message = array( + pht( + 'Manual invoices do not automatically notify recipients yet. '. + 'Send the payer this checkout link:'), + ' ', + phutil_tag( + 'a', + array( + 'href' => $checkout_uri, + ), + $checkout_uri), + ); + + $error_view = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors(array($invoice_message)); + } + break; case PhortuneCart::STATUS_PURCHASING: if ($can_edit) { $resume_uri = $cart->getMetadataValue('provider.checkoutURI'); @@ -86,7 +116,6 @@ final class PhortuneCartViewController $error_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) ->appendChild(pht('This purchase has been completed.')); - break; } @@ -126,6 +155,8 @@ final class PhortuneCartViewController $cart_box->setInfoView($error_view); } + $description = $this->renderCartDescription($cart); + $charges = id(new PhortuneChargeQuery()) ->setViewer($viewer) ->withCartPHIDs(array($cart->getPHID())) @@ -171,6 +202,7 @@ final class PhortuneCartViewController array( $crumbs, $cart_box, + $description, $charges, $timeline, ), diff --git a/src/applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php b/src/applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php new file mode 100644 index 0000000000..9fefdc5ae4 --- /dev/null +++ b/src/applications/phortune/controller/PhortuneMerchantInvoiceCreateController.php @@ -0,0 +1,245 @@ +getUser(); + + $merchant = $this->loadMerchantAuthority(); + if (!$merchant) { + return new Aphront404Response(); + } + + $merchant_id = $merchant->getID(); + $cancel_uri = $this->getApplicationURI("/merchant/{$merchant_id}/"); + + // Load the user to invoice, or prompt the viewer to select one. + $target_user = null; + $user_phid = head($request->getArr('userPHID')); + if (!$user_phid) { + $user_phid = $request->getStr('userPHID'); + } + if ($user_phid) { + $target_user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($user_phid)) + ->executeOne(); + } + + if (!$target_user) { + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendRemarkupInstructions(pht('Choose a user to invoice.')) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setLabel(pht('User')) + ->setDatasource(new PhabricatorPeopleDatasource()) + ->setName('userPHID') + ->setLimit(1)); + + return $this->newDialog() + ->setTitle(pht('Choose User')) + ->appendForm($form) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Continue')); + } + + // Load the account to invoice, or prompt the viewer to select one. + $target_account = null; + $account_phid = $request->getStr('accountPHID'); + if ($account_phid) { + $target_account = id(new PhortuneAccountQuery()) + ->setViewer($viewer) + ->withPHIDs(array($account_phid)) + ->withMemberPHIDs(array($target_user->getPHID())) + ->executeOne(); + } + + if (!$target_account) { + $accounts = PhortuneAccountQuery::loadAccountsForUser( + $target_user, + PhabricatorContentSource::newFromRequest($request)); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('userPHID', $target_user->getPHID()) + ->appendRemarkupInstructions(pht('Choose which account to invoice.')) + ->appendControl( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('User')) + ->setValue($viewer->renderHandle($target_user->getPHID()))) + ->appendControl( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Account')) + ->setName('accountPHID') + ->setValue($account_phid) + ->setOptions(mpull($accounts, 'getName', 'getPHID'))); + + return $this->newDialog() + ->setTitle(pht('Choose Account')) + ->appendForm($form) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Continue')); + } + + + // Now we build the actual invoice. + $title = pht('New Invoice'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($merchant->getName()); + + $v_title = $request->getStr('title'); + $e_title = true; + + $v_name = $request->getStr('name'); + $e_name = true; + + $v_cost = $request->getStr('cost'); + $e_cost = true; + + $v_desc = $request->getStr('description'); + + $v_quantity = 1; + $e_quantity = null; + + $errors = array(); + if ($request->isFormPost() && $request->getStr('invoice')) { + $v_quantity = $request->getStr('quantity'); + + $e_title = null; + $e_name = null; + $e_cost = null; + $e_quantity = null; + + if (!strlen($v_title)) { + $e_title = pht('Required'); + $errors[] = pht('You must title this invoice.'); + } + + if (!strlen($v_name)) { + $e_name = pht('Required'); + $errors[] = pht('You must provide a name for this purchase.'); + } + + if (!strlen($v_cost)) { + $e_cost = pht('Required'); + $errors[] = pht('You must provide a cost for this purchase.'); + } else { + try { + $v_currency = PhortuneCurrency::newFromUserInput( + $viewer, + $v_cost); + } catch (Exception $ex) { + $errors[] = $ex->getMessage(); + $e_cost = pht('Invalid'); + } + } + + if ((int)$v_quantity <= 0) { + $e_quantity = pht('Invalid'); + $errors[] = pht('Quantity must be a positive integer.'); + } + + if (!$errors) { + $unique = Filesystem::readRandomCharacters(16); + + $product = id(new PhortuneProductQuery()) + ->setViewer($target_user) + ->withClassAndRef('PhortuneAdHocProduct', $unique) + ->executeOne(); + + $cart_implementation = new PhortuneAdHocCart(); + + $cart = $target_account->newCart( + $target_user, + $cart_implementation, + $merchant); + + $cart + ->setMetadataValue('adhoc.title', $v_title) + ->setMetadataValue('adhoc.description', $v_desc); + + $purchase = $cart->newPurchase($target_user, $product) + ->setBasePriceAsCurrency($v_currency) + ->setQuantity((int)$v_quantity) + ->setMetadataValue('adhoc.name', $v_name) + ->save(); + + // TODO: Actually mark these as invoices. Right now, there's no easy + // way to do that. + + $cart->activateCart(); + $cart_id = $cart->getID(); + + $uri = "/merchant/{$merchant_id}/cart/{$cart_id}/?invoice=true"; + $uri = $this->getApplicationURI($uri); + + return id(new AphrontRedirectResponse())->setURI($uri); + } + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('userPHID', $target_user->getPHID()) + ->addHiddenInput('accountPHID', $target_account->getPHID()) + ->addHiddenInput('invoice', true) + ->appendControl( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('User')) + ->setValue($viewer->renderHandle($target_user->getPHID()))) + ->appendControl( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Account')) + ->setValue($viewer->renderHandle($target_account->getPHID()))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Invoice Title')) + ->setName('title') + ->setValue($v_title) + ->setError($e_title)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Purchase Name')) + ->setName('name') + ->setValue($v_name) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Purchase Cost')) + ->setName('cost') + ->setValue($v_cost) + ->setError($e_cost)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Quantity')) + ->setName('quantity') + ->setValue($v_quantity) + ->setError($e_quantity)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Invoice Description')) + ->setName('description') + ->setValue($v_desc)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton($cancel_uri) + ->setValue(pht('Send Invoice'))); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('New Invoice')) + ->setFormErrors($errors) + ->appendChild($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php index 94a53742b0..c1512e9d32 100644 --- a/src/applications/phortune/controller/PhortuneMerchantViewController.php +++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php @@ -192,6 +192,15 @@ final class PhortuneMerchantViewController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + + $view->addAction( + id(new PhabricatorActionView()) + ->setName(pht('New Invoice')) + ->setIcon('fa-fax') + ->setHref($this->getApplicationURI("merchant/{$id}/invoice/new/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + return $view; } diff --git a/src/applications/phortune/product/PhortuneAdHocProduct.php b/src/applications/phortune/product/PhortuneAdHocProduct.php new file mode 100644 index 0000000000..fde10376fb --- /dev/null +++ b/src/applications/phortune/product/PhortuneAdHocProduct.php @@ -0,0 +1,42 @@ + $ref) { + $product = new PhortuneAdHocProduct(); + $product->ref = $ref; + $results[$key] = $product; + } + + return $results; + } + + public function getRef() { + return $this->ref; + } + + public function getName(PhortuneProduct $product) { + return pht('Ad-Hoc Product'); + } + + public function getPurchaseName( + PhortuneProduct $product, + PhortunePurchase $purchase) { + + return coalesce( + $purchase->getMetadataValue('adhoc.name'), + $this->getName($product)); + } + + public function getPriceAsCurrency(PhortuneProduct $product) { + return PhortuneCurrency::newEmptyCurrency(); + } + +} diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php index 8a66a29787..9cf7c51e0b 100644 --- a/src/applications/phortune/storage/PhortuneCart.php +++ b/src/applications/phortune/storage/PhortuneCart.php @@ -453,6 +453,10 @@ final class PhortuneCart extends PhortuneDAO return $this->getImplementation()->getCancelURI($this); } + public function getDescription() { + return $this->getImplementation()->getDescription($this); + } + public function getDetailURI(PhortuneMerchant $authority = null) { if ($authority) { $prefix = 'merchant/'.$authority->getID().'/'; diff --git a/src/applications/phortune/storage/PhortunePurchase.php b/src/applications/phortune/storage/PhortunePurchase.php index ff0a5287ec..df930820a1 100644 --- a/src/applications/phortune/storage/PhortunePurchase.php +++ b/src/applications/phortune/storage/PhortunePurchase.php @@ -92,7 +92,14 @@ final class PhortunePurchase extends PhortuneDAO } public function getTotalPriceAsCurrency() { - return $this->getBasePriceAsCurrency(); + $base = $this->getBasePriceAsCurrency(); + + $price = PhortuneCurrency::newEmptyCurrency(); + for ($ii = 0; $ii < $this->getQuantity(); $ii++) { + $price = $price->add($base); + } + + return $price; } public function getMetadataValue($key, $default = null) {