Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/phortune/query/PhortunePaymentMethodQuery.php b/src/applications/phortune/query/PhortunePaymentMethodQuery.php
index 07d49a5909..42d54805e6 100644
--- a/src/applications/phortune/query/PhortunePaymentMethodQuery.php
+++ b/src/applications/phortune/query/PhortunePaymentMethodQuery.php
@@ -1,156 +1,147 @@
<?php
final class PhortunePaymentMethodQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $accountPHIDs;
private $merchantPHIDs;
private $statuses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withAccountPHIDs(array $phids) {
$this->accountPHIDs = $phids;
return $this;
}
public function withMerchantPHIDs(array $phids) {
$this->merchantPHIDs = $phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
+ public function newResultObject() {
+ return new PhortunePaymentMethod();
+ }
+
protected function loadPage() {
- $table = new PhortunePaymentMethod();
- $conn = $table->establishConnection('r');
-
- $rows = queryfx_all(
- $conn,
- 'SELECT * FROM %T %Q %Q %Q',
- $table->getTableName(),
- $this->buildWhereClause($conn),
- $this->buildOrderClause($conn),
- $this->buildLimitClause($conn));
-
- return $table->loadAllFromArray($rows);
+ return $this->loadStandardPage($this->newResultObject());
}
protected function willFilterPage(array $methods) {
$accounts = id(new PhortuneAccountQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getAccountPHID'))
->execute();
$accounts = mpull($accounts, null, 'getPHID');
foreach ($methods as $key => $method) {
$account = idx($accounts, $method->getAccountPHID());
if (!$account) {
unset($methods[$key]);
continue;
}
$method->attachAccount($account);
}
if (!$methods) {
return $methods;
}
$merchants = id(new PhortuneMerchantQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getMerchantPHID'))
->execute();
$merchants = mpull($merchants, null, 'getPHID');
foreach ($methods as $key => $method) {
$merchant = idx($merchants, $method->getMerchantPHID());
if (!$merchant) {
unset($methods[$key]);
continue;
}
$method->attachMerchant($merchant);
}
if (!$methods) {
return $methods;
}
$provider_configs = id(new PhortunePaymentProviderConfigQuery())
->setViewer($this->getViewer())
->withPHIDs(mpull($methods, 'getProviderPHID'))
->execute();
$provider_configs = mpull($provider_configs, null, 'getPHID');
foreach ($methods as $key => $method) {
$provider_config = idx($provider_configs, $method->getProviderPHID());
if (!$provider_config) {
unset($methods[$key]);
continue;
}
$method->attachProviderConfig($provider_config);
}
return $methods;
}
- protected function buildWhereClause(AphrontDatabaseConnection $conn) {
- $where = array();
+ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
+ $where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->accountPHIDs !== null) {
$where[] = qsprintf(
$conn,
'accountPHID IN (%Ls)',
$this->accountPHIDs);
}
if ($this->merchantPHIDs !== null) {
$where[] = qsprintf(
$conn,
'merchantPHID IN (%Ls)',
$this->merchantPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
- $where[] = $this->buildPagingClause($conn);
-
- return $this->formatWhereClause($where);
+ return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorPhortuneApplication';
}
}
diff --git a/src/applications/phortune/storage/PhortuneCart.php b/src/applications/phortune/storage/PhortuneCart.php
index af2b386dfe..07554ccb87 100644
--- a/src/applications/phortune/storage/PhortuneCart.php
+++ b/src/applications/phortune/storage/PhortuneCart.php
@@ -1,693 +1,700 @@
<?php
final class PhortuneCart extends PhortuneDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface {
const STATUS_BUILDING = 'cart:building';
const STATUS_READY = 'cart:ready';
const STATUS_PURCHASING = 'cart:purchasing';
const STATUS_CHARGED = 'cart:charged';
const STATUS_HOLD = 'cart:hold';
const STATUS_REVIEW = 'cart:review';
const STATUS_PURCHASED = 'cart:purchased';
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $subscriptionPHID;
protected $cartClass;
protected $status;
protected $metadata = array();
protected $mailKey;
protected $isInvoice;
private $account = self::ATTACHABLE;
private $purchases = self::ATTACHABLE;
private $implementation = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
public static function initializeNewCart(
PhabricatorUser $actor,
PhortuneAccount $account,
PhortuneMerchant $merchant) {
$cart = id(new PhortuneCart())
->setAuthorPHID($actor->getPHID())
->setStatus(self::STATUS_BUILDING)
->setAccountPHID($account->getPHID())
->setIsInvoice(0)
->attachAccount($account)
->setMerchantPHID($merchant->getPHID())
->attachMerchant($merchant);
$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 static function getStatusNameMap() {
return array(
self::STATUS_BUILDING => pht('Building'),
self::STATUS_READY => pht('Ready'),
self::STATUS_PURCHASING => pht('Purchasing'),
self::STATUS_CHARGED => pht('Charged'),
self::STATUS_HOLD => pht('Hold'),
self::STATUS_REVIEW => pht('Review'),
self::STATUS_PURCHASED => pht('Purchased'),
);
}
public static function getNameForStatus($status) {
return idx(self::getStatusNameMap(), $status, $status);
}
public function activateCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if ($copy->getStatus() !== self::STATUS_BUILDING) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'willApplyCharge()'));
}
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED);
return $this;
}
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())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setAmountAsCurrency($this->getTotalPriceAsCurrency());
if ($method) {
+ if (!$method->isActive()) {
+ throw new Exception(
+ pht(
+ 'Attempting to apply a charge using an inactive '.
+ 'payment method ("%s")!',
+ $method->getPHID()));
+ }
$charge->setPaymentMethodPHID($method->getPHID());
}
$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 %s, expected "%s".',
$copy->getStatus(),
'willApplyCharge()',
self::STATUS_READY));
}
$charge->save();
$this->setStatus(self::STATUS_PURCHASING)->save();
$this->endReadLocking();
$this->saveTransaction();
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 %s, expected "%s".',
$copy->getStatus(),
'didHoldCharge()',
self::STATUS_PURCHASING));
}
$charge->save();
$this->setStatus(self::STATUS_HOLD)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD);
}
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) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'didApplyCharge()'));
}
$charge->save();
$this->setStatus(self::STATUS_CHARGED)->save();
$this->endReadLocking();
$this->saveTransaction();
// TODO: Perform purchase review. Here, we would apply rules to determine
// whether the charge needs manual review (maybe making the decision via
// Herald, configuration, or by examining provider fraud data). For now,
// don't require review.
$needs_review = false;
if ($needs_review) {
$this->willReviewCart();
} else {
$this->didReviewCart();
}
return $this;
}
public function willReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s!',
$copy->getStatus(),
'willReviewCart()'));
}
$this->setStatus(self::STATUS_REVIEW)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW);
return $this;
}
public function didReviewCart() {
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_CHARGED) &&
($copy->getStatus() !== self::STATUS_REVIEW)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s!',
$copy->getStatus(),
'didReviewCart()'));
}
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didPurchaseProduct($purchase);
}
$this->setStatus(self::STATUS_PURCHASED)->save();
$this->endReadLocking();
$this->saveTransaction();
$this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED);
return $this;
}
public function didFailCharge(PhortuneCharge $charge) {
$charge->setStatus(PhortuneCharge::STATUS_FAILED);
$this->openTransaction();
$this->beginReadLocking();
$copy = clone $this;
$copy->reload();
if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
($copy->getStatus() !== self::STATUS_HOLD)) {
throw new Exception(
pht(
'Cart has wrong status ("%s") to call %s.',
$copy->getStatus(),
'didFailCharge()'));
}
$charge->save();
// Move the cart back into STATUS_READY so the user can try
// making the purchase again.
$this->setStatus(self::STATUS_READY)->save();
$this->endReadLocking();
$this->saveTransaction();
return $this;
}
public function willRefundCharge(
PhabricatorUser $actor,
PhortunePaymentProvider $provider,
PhortuneCharge $charge,
PhortuneCurrency $amount) {
if (!$amount->isPositive()) {
throw new Exception(
pht('Trying to refund non-positive 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) &&
($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
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();
$amount = $refund->getAmountAsCurrency()->negate();
foreach ($this->purchases as $purchase) {
$purchase->getProduct()->didRefundProduct($purchase, $amount);
}
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();
}
private function recordCartTransaction($type) {
$omnipotent_user = PhabricatorUser::getOmnipotentUser();
$phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID();
$xactions = array();
$xactions[] = id(new PhortuneCartTransaction())
->setTransactionType($type)
->setNewValue(true);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorPhortuneContentSource::SOURCECONST);
$editor = id(new PhortuneCartEditor())
->setActor($omnipotent_user)
->setActingAsPHID($phortune_phid)
->setContentSource($content_source)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true);
$editor->applyTransactions($this, $xactions);
}
public function getName() {
return $this->getImplementation()->getName($this);
}
public function getDoneURI() {
return $this->getImplementation()->getDoneURI($this);
}
public function getDoneActionName() {
return $this->getImplementation()->getDoneActionName($this);
}
public function getCancelURI() {
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().'/';
} else {
$prefix = '';
}
return '/phortune/'.$prefix.'cart/'.$this->getID().'/';
}
public function getCheckoutURI() {
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);
}
protected 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',
'mailKey' => 'bytes20',
'subscriptionPHID' => 'phid?',
'isInvoice' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID'),
),
'key_merchant' => array(
'columns' => array('merchantPHID'),
),
'key_subscription' => array(
'columns' => array('subscriptionPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortuneCartPHIDType::TYPECONST);
}
public function save() {
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
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 attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
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);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhortuneCartEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhortuneCartTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function 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) {
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) {
return array(
pht('Orders inherit the policies of the associated account.'),
pht('The merchant you placed an order with can review and manage it.'),
);
}
}
diff --git a/src/applications/phortune/storage/PhortunePaymentMethod.php b/src/applications/phortune/storage/PhortunePaymentMethod.php
index 8044168ba4..1712d3f973 100644
--- a/src/applications/phortune/storage/PhortunePaymentMethod.php
+++ b/src/applications/phortune/storage/PhortunePaymentMethod.php
@@ -1,157 +1,161 @@
<?php
/**
* A payment method is a credit card; it is associated with an account and
* charges can be made against it.
*/
final class PhortunePaymentMethod extends PhortuneDAO
implements PhabricatorPolicyInterface {
const STATUS_ACTIVE = 'payment:active';
const STATUS_DISABLED = 'payment:disabled';
protected $name = '';
protected $status;
protected $accountPHID;
protected $authorPHID;
protected $merchantPHID;
protected $providerPHID;
protected $expires;
protected $metadata = array();
protected $brand;
protected $lastFourDigits;
private $account = self::ATTACHABLE;
private $merchant = self::ATTACHABLE;
private $providerConfig = self::ATTACHABLE;
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255',
'status' => 'text64',
'brand' => 'text64',
'expires' => 'text16',
'lastFourDigits' => 'text16',
),
self::CONFIG_KEY_SCHEMA => array(
'key_account' => array(
'columns' => array('accountPHID', 'status'),
),
'key_merchant' => array(
'columns' => array('merchantPHID', 'accountPHID'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhortunePaymentMethodPHIDType::TYPECONST);
}
public function attachAccount(PhortuneAccount $account) {
$this->account = $account;
return $this;
}
public function getAccount() {
return $this->assertAttached($this->account);
}
public function attachMerchant(PhortuneMerchant $merchant) {
$this->merchant = $merchant;
return $this;
}
public function getMerchant() {
return $this->assertAttached($this->merchant);
}
public function attachProviderConfig(PhortunePaymentProviderConfig $config) {
$this->providerConfig = $config;
return $this;
}
public function getProviderConfig() {
return $this->assertAttached($this->providerConfig);
}
public function getDescription() {
$provider = $this->buildPaymentProvider();
return $provider->getPaymentMethodProviderDescription();
}
public function getMetadataValue($key, $default = null) {
return idx($this->getMetadata(), $key, $default);
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function buildPaymentProvider() {
return $this->getProviderConfig()->buildProvider();
}
public function getDisplayName() {
if (strlen($this->name)) {
return $this->name;
}
$provider = $this->buildPaymentProvider();
return $provider->getDefaultPaymentMethodDisplayName($this);
}
public function getFullDisplayName() {
return pht('%s (%s)', $this->getDisplayName(), $this->getSummary());
}
public function getSummary() {
return pht('%s %s', $this->getBrand(), $this->getLastFourDigits());
}
public function setExpires($year, $month) {
$this->expires = $year.'-'.$month;
return $this;
}
public function getDisplayExpires() {
list($year, $month) = explode('-', $this->getExpires());
$month = sprintf('%02d', $month);
$year = substr($year, -2);
return $month.'/'.$year;
}
+ public function isActive() {
+ return ($this->getStatus() === self::STATUS_ACTIVE);
+ }
+
/* -( 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(
'Members of an account can always view and edit its payment methods.');
}
}
diff --git a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
index 097fb54dd9..d05aacbb7c 100644
--- a/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
+++ b/src/applications/phortune/worker/PhortuneSubscriptionWorker.php
@@ -1,237 +1,241 @@
<?php
final class PhortuneSubscriptionWorker extends PhabricatorWorker {
protected function doWork() {
$subscription = $this->loadSubscription();
$range = $this->getBillingPeriodRange($subscription);
list($last_epoch, $next_epoch) = $range;
$should_invoice = $subscription->shouldInvoiceForBillingPeriod(
$last_epoch,
$next_epoch);
if (!$should_invoice) {
return;
}
$currency = $subscription->getCostForBillingPeriodAsCurrency(
$last_epoch,
$next_epoch);
if (!$currency->isPositive()) {
return;
}
$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 a fairly arbitrary account member.
// 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.
$members = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs($account->getMemberPHIDs())
->execute();
$actor = null;
foreach ($members as $member) {
// Don't act as a disabled user. If all of the users on the account are
// disabled this means we won't charge the subscription, but that's
// probably correct since it means no one can cancel or pay it anyway.
if ($member->getIsDisabled()) {
continue;
}
// For now, just pick the first valid user we encounter as the actor.
$actor = $member;
break;
}
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);
$purchase
->setBasePriceAsCurrency($currency)
->setMetadataValue('subscriptionPHID', $subscription->getPHID())
->setMetadataValue('epoch.start', $last_epoch)
->setMetadataValue('epoch.end', $next_epoch)
->save();
$cart
->setSubscriptionPHID($subscription->getPHID())
->setIsInvoice(1)
->save();
$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;
}
// 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(
PhabricatorPhortuneContentSource::SOURCECONST);
$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()))
+ ->withStatuses(
+ array(
+ PhortunePaymentMethod::STATUS_ACTIVE,
+ ))
->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.this-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 (empty($data['manual'])) {
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
Fri, Mar 14, 2:22 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
71943
Default Alt Text
(36 KB)

Event Timeline