Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/phortune/editor/PhortuneCartEditor.php b/src/applications/phortune/editor/PhortuneCartEditor.php
index fb39102624..0597c82d87 100644
--- a/src/applications/phortune/editor/PhortuneCartEditor.php
+++ b/src/applications/phortune/editor/PhortuneCartEditor.php
@@ -1,167 +1,223 @@
<?php
final class PhortuneCartEditor
extends PhabricatorApplicationTransactionEditor {
+ private $invoiceIssues;
+
+ public function setInvoiceIssues(array $invoice_issues) {
+ $this->invoiceIssues = $invoice_issues;
+ return $this;
+ }
+
+ public function getInvoiceIssues() {
+ return $this->invoiceIssues;
+ }
+
+ public function isInvoice() {
+ return (bool)$this->invoiceIssues;
+ }
+
public function getEditorApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
public function getEditorObjectsDescription() {
return pht('Phortune Carts');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhortuneCartTransaction::TYPE_CREATED;
$types[] = PhortuneCartTransaction::TYPE_PURCHASED;
$types[] = PhortuneCartTransaction::TYPE_HOLD;
$types[] = PhortuneCartTransaction::TYPE_REVIEW;
$types[] = PhortuneCartTransaction::TYPE_CANCEL;
$types[] = PhortuneCartTransaction::TYPE_REFUND;
+ $types[] = PhortuneCartTransaction::TYPE_INVOICED;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
+ case PhortuneCartTransaction::TYPE_INVOICED:
return null;
}
return parent::getCustomTransactionOldValue($object, $xaction);
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
+ case PhortuneCartTransaction::TYPE_INVOICED:
return $xaction->getNewValue();
}
return parent::getCustomTransactionNewValue($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
+ case PhortuneCartTransaction::TYPE_INVOICED:
return;
}
return parent::applyCustomInternalTransaction($object, $xaction);
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case PhortuneCartTransaction::TYPE_CREATED:
case PhortuneCartTransaction::TYPE_PURCHASED:
case PhortuneCartTransaction::TYPE_HOLD:
case PhortuneCartTransaction::TYPE_REVIEW:
case PhortuneCartTransaction::TYPE_CANCEL:
case PhortuneCartTransaction::TYPE_REFUND:
+ case PhortuneCartTransaction::TYPE_INVOICED:
return;
}
return parent::applyCustomExternalTransaction($object, $xaction);
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$name = $object->getName();
return id(new PhabricatorMetaMTAMail())
->setSubject(pht('Order %d: %s', $id, $name))
->addHeader('Thread-Topic', pht('Order %s', $id));
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
+ if ($this->isInvoice()) {
+ $issues = $this->getInvoiceIssues();
+ foreach ($issues as $key => $issue) {
+ $issues[$key] = ' - '.$issue;
+ }
+ $issues = implode("\n", $issues);
+
+ $overview = pht(
+ "Payment for this invoice could not be processed automatically:\n\n".
+ "%s",
+ $issues);
+
+ $body->addRemarkupSection($overview);
+
+ $body->addLinkSection(
+ pht('PAY NOW'),
+ PhabricatorEnv::getProductionURI($object->getCheckoutURI()));
+ }
+
$items = array();
foreach ($object->getPurchases() as $purchase) {
$name = $purchase->getFullDisplayName();
$price = $purchase->getTotalPriceAsCurrency()->formatForDisplay();
$items[] = "{$name} {$price}";
}
$body->addTextSection(pht('ORDER CONTENTS'), implode("\n", $items));
+ if ($this->isInvoice()) {
+ $subscription = id(new PhortuneSubscriptionQuery())
+ ->setViewer($this->requireActor())
+ ->withPHIDs(array($object->getSubscriptionPHID()))
+ ->executeOne();
+ if ($subscription) {
+ $body->addLinkSection(
+ pht('SUBSCRIPTION'),
+ PhabricatorEnv::getProductionURI($subscription->getURI()));
+ }
+ } else {
+ $body->addLinkSection(
+ pht('ORDER DETAIL'),
+ PhabricatorEnv::getProductionURI($object->getDetailURI()));
+ }
+
+ $account_uri = '/phortune/'.$object->getAccount()->getID().'/';
$body->addLinkSection(
- pht('ORDER DETAIL'),
- PhabricatorEnv::getProductionURI('/phortune/cart/'.$object->getID().'/'));
+ pht('ACCOUNT OVERVIEW'),
+ PhabricatorEnv::getProductionURI($account_uri));
return $body;
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
// Relaod the cart to pull merchant and account information, in case we
// just created the object.
$cart = id(new PhortuneCartQuery())
->setViewer($this->requireActor())
->withPHIDs(array($object->getPHID()))
->executeOne();
foreach ($cart->getAccount()->getMemberPHIDs() as $account_member) {
$phids[] = $account_member;
}
foreach ($cart->getMerchant()->getMemberPHIDs() as $merchant_member) {
$phids[] = $merchant_member;
}
return $phids;
}
protected function getMailCC(PhabricatorLiskDAO $object) {
return array();
}
protected function getMailSubjectPrefix() {
- return 'Order';
+ return '[Phortune]';
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new PhortuneCartReplyHandler())
->setMailReceiver($object);
}
}
diff --git a/src/applications/phortune/storage/PhortuneCartTransaction.php b/src/applications/phortune/storage/PhortuneCartTransaction.php
index 5638031118..41790011a2 100644
--- a/src/applications/phortune/storage/PhortuneCartTransaction.php
+++ b/src/applications/phortune/storage/PhortuneCartTransaction.php
@@ -1,57 +1,91 @@
<?php
final class PhortuneCartTransaction
extends PhabricatorApplicationTransaction {
const TYPE_CREATED = 'cart:created';
const TYPE_HOLD = 'cart:hold';
const TYPE_REVIEW = 'cart:review';
const TYPE_CANCEL = 'cart:cancel';
const TYPE_REFUND = 'cart:refund';
const TYPE_PURCHASED = 'cart:purchased';
+ const TYPE_INVOICED = 'cart:invoiced';
public function getApplicationName() {
return 'phortune';
}
public function getApplicationTransactionType() {
return PhortuneCartPHIDType::TYPECONST;
}
public function getApplicationTransactionCommentObject() {
return null;
}
public function shouldHideForMail(array $xactions) {
switch ($this->getTransactionType()) {
case self::TYPE_CREATED:
return true;
}
return parent::shouldHideForMail($xactions);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
switch ($this->getTransactionType()) {
case self::TYPE_CREATED:
return pht('This order was created.');
case self::TYPE_HOLD:
return pht('This order was put on hold until payment clears.');
case self::TYPE_REVIEW:
return pht(
'This order was flagged for manual processing by the merchant.');
case self::TYPE_CANCEL:
return pht('This order was cancelled.');
case self::TYPE_REFUND:
return pht('This order was refunded.');
case self::TYPE_PURCHASED:
return pht('Payment for this order was completed.');
+ case self::TYPE_INVOICED:
+ return pht('This order was invoiced.');
}
return parent::getTitle();
}
+ public function getTitleForMail() {
+ switch ($this->getTransactionType()) {
+ case self::TYPE_INVOICED:
+ return pht('You have a new invoice due.');
+ }
+
+ return parent::getTitleForMail();
+ }
+
+ public function getActionName() {
+ switch ($this->getTransactionType()) {
+ case self::TYPE_CREATED:
+ return pht('Created');
+ case self::TYPE_HOLD:
+ return pht('Hold');
+ case self::TYPE_REVIEW:
+ return pht('Review');
+ case self::TYPE_CANCEL:
+ return pht('Cancelled');
+ case self::TYPE_REFUND:
+ return pht('Refunded');
+ case self::TYPE_PURCHASED:
+ return pht('Complete');
+ case self::TYPE_INVOICED:
+ return pht('New Invoice');
+ }
+
+ return parent::getActionName();
+ }
+
+
}
diff --git a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
index 9189087481..b94b245df8 100644
--- a/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
+++ b/src/applications/phortune/subscription/PhortuneSubscriptionImplementation.php
@@ -1,50 +1,44 @@
<?php
abstract class PhortuneSubscriptionImplementation {
abstract public function loadImplementationsForRefs(
PhabricatorUser $viewer,
array $refs);
abstract public function getRef();
abstract public function getName(PhortuneSubscription $subscription);
public function getFullName(PhortuneSubscription $subscription) {
return $this->getName($subscription);
}
public function getCrumbName(PhortuneSubscription $subscription) {
return $this->getName($subscription);
}
abstract public function getCostForBillingPeriodAsCurrency(
PhortuneSubscription $subscription,
$start_epoch,
$end_epoch);
- protected function getContentSource() {
- return PhabricatorContentSource::newForSource(
- PhabricatorContentSource::SOURCE_PHORTUNE,
- array());
- }
-
public function getCartName(
PhortuneSubscription $subscription,
PhortuneCart $cart) {
return pht('Subscription');
}
public function getPurchaseName(
PhortuneSubscription $subscription,
PhortuneProduct $product,
PhortunePurchase $purchase) {
return $product->getProductName();
}
public function getPurchaseURI(
PhortuneSubscription $subscription,
PhortuneProduct $product,
PhortunePurchase $purchase) {
return null;
}
}
diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
index bb3725fec8..272be2c5a2 100644
--- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
+++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
@@ -1,191 +1,209 @@
<?php
final class PhortuneSubscriptionWorker extends PhabricatorWorker {
protected function doWork() {
$subscription = $this->loadSubscription();
$range = $this->getBillingPeriodRange($subscription);
list($last_epoch, $next_epoch) = $range;
$account = $subscription->getAccount();
$merchant = $subscription->getMerchant();
$viewer = PhabricatorUser::getOmnipotentUser();
$product = id(new PhortuneProductQuery())
->setViewer($viewer)
->withClassAndRef('PhortuneSubscriptionProduct', $subscription->getPHID())
->executeOne();
$cart_implementation = id(new PhortuneSubscriptionCart())
->setSubscription($subscription);
// TODO: This isn't really ideal. It would be better to use an application
// actor than the original author of the subscription. In particular, if
// someone initiates a subscription, adds some other account managers, and
// later leaves the company, they'll continue "acting" here indefinitely.
// However, for now, some of the stuff later in the pipeline requires a
// valid actor with a real PHID. The subscription should eventually be
// able to create these invoices "as" the application it is acting on
// behalf of.
$actor = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($subscription->getAuthorPHID()))
->executeOne();
if (!$actor) {
throw new Exception(pht('Failed to load actor to bill subscription!'));
}
$cart = $account->newCart($actor, $cart_implementation, $merchant);
$purchase = $cart->newPurchase($actor, $product);
$currency = $subscription->getCostForBillingPeriodAsCurrency(
$last_epoch,
$next_epoch);
$purchase
->setBasePriceAsCurrency($currency)
->setMetadataValue('subscriptionPHID', $subscription->getPHID())
->setMetadataValue('epoch.start', $last_epoch)
->setMetadataValue('epoch.end', $next_epoch)
->save();
$cart->setSubscriptionPHID($subscription->getPHID());
$cart->activateCart();
try {
$issues = $this->chargeSubscription($actor, $subscription, $cart);
} catch (Exception $ex) {
$issues = array(
pht(
'There was a technical error while trying to automatically bill '.
'this subscription: %s',
$ex),
);
}
if (!$issues) {
// We're all done; charging the cart sends a billing email as a side
// effect.
return;
}
- // TODO: Send an email telling the user that we weren't able to autopay
- // so they need to pay this manually.
- throw new Exception(implode("\n", $issues));
+ // We're shoving this through the CartEditor because it has all the logic
+ // for sending mail about carts. This doesn't really affect the state of
+ // the cart, but reduces the amount of code duplication.
+
+ $xactions = array();
+ $xactions[] = id(new PhortuneCartTransaction())
+ ->setTransactionType(PhortuneCartTransaction::TYPE_INVOICED)
+ ->setNewValue(true);
+
+ $content_source = PhabricatorContentSource::newForSource(
+ PhabricatorContentSource::SOURCE_PHORTUNE,
+ array());
+
+ $acting_phid = id(new PhabricatorPhortuneApplication())->getPHID();
+ $editor = id(new PhortuneCartEditor())
+ ->setActor($viewer)
+ ->setActingAsPHID($acting_phid)
+ ->setContentSource($content_source)
+ ->setContinueOnMissingFields(true)
+ ->setInvoiceIssues($issues)
+ ->applyTransactions($cart, $xactions);
}
private function chargeSubscription(
PhabricatorUser $viewer,
PhortuneSubscription $subscription,
PhortuneCart $cart) {
$issues = array();
if (!$subscription->getDefaultPaymentMethodPHID()) {
$issues[] = pht(
'There is no payment method associated with this subscription, so '.
'it could not be billed automatically. Add a default payment method '.
'to enable automatic billing.');
return $issues;
}
$method = id(new PhortunePaymentMethodQuery())
->setViewer($viewer)
->withPHIDs(array($subscription->getDefaultPaymentMethodPHID()))
->executeOne();
if (!$method) {
$issues[] = pht(
'The payment method associated with this subscription is invalid '.
'or out of date, so it could not be automatically billed. Update '.
'the default payment method to enable automatic billing.');
return $issues;
}
$provider = $method->buildPaymentProvider();
$charge = $cart->willApplyCharge($viewer, $provider, $method);
try {
$provider->applyCharge($method, $charge);
} catch (Exception $ex) {
$cart->didFailCharge($charge);
$issues[] = pht(
'Automatic billing failed: %s',
$ex->getMessage());
return $issues;
}
$cart->didApplyCharge($charge);
}
/**
* Load the subscription to generate an invoice for.
*
* @return PhortuneSubscription The subscription to invoice.
*/
private function loadSubscription() {
$viewer = PhabricatorUser::getOmnipotentUser();
$data = $this->getTaskData();
$subscription_phid = idx($data, 'subscriptionPHID');
$subscription = id(new PhortuneSubscriptionQuery())
->setViewer($viewer)
->withPHIDs(array($subscription_phid))
->executeOne();
if (!$subscription) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Failed to load subscription with PHID "%s".',
$subscription_phid));
}
return $subscription;
}
/**
* Get the start and end epoch timestamps for this billing period.
*
* @param PhortuneSubscription The subscription being billed.
* @return pair<int, int> Beginning and end of the billing range.
*/
private function getBillingPeriodRange(PhortuneSubscription $subscription) {
$data = $this->getTaskData();
$last_epoch = idx($data, 'trigger.last-epoch');
if (!$last_epoch) {
// If this is the first time the subscription is firing, use the
// creation date as the start of the billing period.
$last_epoch = $subscription->getDateCreated();
}
$this_epoch = idx($data, 'trigger.next-epoch');
if (!$last_epoch || !$this_epoch) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Subscription is missing billing period information.'));
}
$period_length = ($this_epoch - $last_epoch);
if ($period_length <= 0) {
throw new PhabricatorWorkerPermanentFailureException(
pht(
'Subscription has invalid billing period.'));
}
if (PhabricatorTime::getNow() < $this_epoch) {
throw new Exception(
pht(
'Refusing to generate a subscription invoice for a billing period '.
'which ends in the future.'));
}
return array($last_epoch, $this_epoch);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Dec 2, 10:24 PM (4 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
432878
Default Alt Text
(18 KB)

Event Timeline