[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/phortune/storage/ -> PhortuneCart.php (source)

   1  <?php
   2  
   3  final class PhortuneCart extends PhortuneDAO
   4    implements PhabricatorPolicyInterface {
   5  
   6    const STATUS_BUILDING = 'cart:building';
   7    const STATUS_READY = 'cart:ready';
   8    const STATUS_PURCHASING = 'cart:purchasing';
   9    const STATUS_CHARGED = 'cart:charged';
  10    const STATUS_HOLD = 'cart:hold';
  11    const STATUS_REVIEW = 'cart:review';
  12    const STATUS_PURCHASED = 'cart:purchased';
  13  
  14    protected $accountPHID;
  15    protected $authorPHID;
  16    protected $merchantPHID;
  17    protected $cartClass;
  18    protected $status;
  19    protected $metadata = array();
  20    protected $mailKey;
  21  
  22    private $account = self::ATTACHABLE;
  23    private $purchases = self::ATTACHABLE;
  24    private $implementation = self::ATTACHABLE;
  25    private $merchant = self::ATTACHABLE;
  26  
  27    public static function initializeNewCart(
  28      PhabricatorUser $actor,
  29      PhortuneAccount $account,
  30      PhortuneMerchant $merchant) {
  31      $cart = id(new PhortuneCart())
  32        ->setAuthorPHID($actor->getPHID())
  33        ->setStatus(self::STATUS_BUILDING)
  34        ->setAccountPHID($account->getPHID())
  35        ->setMerchantPHID($merchant->getPHID());
  36  
  37      $cart->account = $account;
  38      $cart->purchases = array();
  39  
  40      return $cart;
  41    }
  42  
  43    public function newPurchase(
  44      PhabricatorUser $actor,
  45      PhortuneProduct $product) {
  46  
  47      $purchase = PhortunePurchase::initializeNewPurchase($actor, $product)
  48        ->setAccountPHID($this->getAccount()->getPHID())
  49        ->setCartPHID($this->getPHID())
  50        ->save();
  51  
  52      $this->purchases[] = $purchase;
  53  
  54      return $purchase;
  55    }
  56  
  57    public static function getStatusNameMap() {
  58      return array(
  59        self::STATUS_BUILDING => pht('Building'),
  60        self::STATUS_READY => pht('Ready'),
  61        self::STATUS_PURCHASING => pht('Purchasing'),
  62        self::STATUS_CHARGED => pht('Charged'),
  63        self::STATUS_HOLD => pht('Hold'),
  64        self::STATUS_REVIEW => pht('Review'),
  65        self::STATUS_PURCHASED => pht('Purchased'),
  66      );
  67    }
  68  
  69    public static function getNameForStatus($status) {
  70      return idx(self::getStatusNameMap(), $status, $status);
  71    }
  72  
  73    public function activateCart() {
  74      $this->openTransaction();
  75        $this->beginReadLocking();
  76  
  77          $copy = clone $this;
  78          $copy->reload();
  79  
  80          if ($copy->getStatus() !== self::STATUS_BUILDING) {
  81            throw new Exception(
  82              pht(
  83                'Cart has wrong status ("%s") to call willApplyCharge().',
  84                $copy->getStatus()));
  85          }
  86  
  87          $this->setStatus(self::STATUS_READY)->save();
  88  
  89        $this->endReadLocking();
  90      $this->saveTransaction();
  91  
  92      $this->recordCartTransaction(PhortuneCartTransaction::TYPE_CREATED);
  93  
  94      return $this;
  95    }
  96  
  97    public function willApplyCharge(
  98      PhabricatorUser $actor,
  99      PhortunePaymentProvider $provider,
 100      PhortunePaymentMethod $method = null) {
 101  
 102      $account = $this->getAccount();
 103  
 104      $charge = PhortuneCharge::initializeNewCharge()
 105        ->setAccountPHID($account->getPHID())
 106        ->setCartPHID($this->getPHID())
 107        ->setAuthorPHID($actor->getPHID())
 108        ->setMerchantPHID($this->getMerchant()->getPHID())
 109        ->setProviderPHID($provider->getProviderConfig()->getPHID())
 110        ->setAmountAsCurrency($this->getTotalPriceAsCurrency());
 111  
 112      if ($method) {
 113        $charge->setPaymentMethodPHID($method->getPHID());
 114      }
 115  
 116      $this->openTransaction();
 117        $this->beginReadLocking();
 118  
 119          $copy = clone $this;
 120          $copy->reload();
 121  
 122          if ($copy->getStatus() !== self::STATUS_READY) {
 123            throw new Exception(
 124              pht(
 125                'Cart has wrong status ("%s") to call willApplyCharge(), '.
 126                'expected "%s".',
 127                $copy->getStatus(),
 128                self::STATUS_READY));
 129          }
 130  
 131          $charge->save();
 132          $this->setStatus(PhortuneCart::STATUS_PURCHASING)->save();
 133  
 134        $this->endReadLocking();
 135      $this->saveTransaction();
 136  
 137      return $charge;
 138    }
 139  
 140    public function didHoldCharge(PhortuneCharge $charge) {
 141      $charge->setStatus(PhortuneCharge::STATUS_HOLD);
 142  
 143      $this->openTransaction();
 144        $this->beginReadLocking();
 145  
 146          $copy = clone $this;
 147          $copy->reload();
 148  
 149          if ($copy->getStatus() !== self::STATUS_PURCHASING) {
 150            throw new Exception(
 151              pht(
 152                'Cart has wrong status ("%s") to call didHoldCharge(), '.
 153                'expected "%s".',
 154                $copy->getStatus(),
 155                self::STATUS_PURCHASING));
 156          }
 157  
 158          $charge->save();
 159          $this->setStatus(self::STATUS_HOLD)->save();
 160  
 161        $this->endReadLocking();
 162      $this->saveTransaction();
 163  
 164      $this->recordCartTransaction(PhortuneCartTransaction::TYPE_HOLD);
 165    }
 166  
 167    public function didApplyCharge(PhortuneCharge $charge) {
 168      $charge->setStatus(PhortuneCharge::STATUS_CHARGED);
 169  
 170      $this->openTransaction();
 171        $this->beginReadLocking();
 172  
 173          $copy = clone $this;
 174          $copy->reload();
 175  
 176          if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
 177              ($copy->getStatus() !== self::STATUS_HOLD)) {
 178            throw new Exception(
 179              pht(
 180                'Cart has wrong status ("%s") to call didApplyCharge().',
 181                $copy->getStatus()));
 182          }
 183  
 184          $charge->save();
 185          $this->setStatus(self::STATUS_CHARGED)->save();
 186  
 187        $this->endReadLocking();
 188      $this->saveTransaction();
 189  
 190      // TODO: Perform purchase review. Here, we would apply rules to determine
 191      // whether the charge needs manual review (maybe making the decision via
 192      // Herald, configuration, or by examining provider fraud data). For now,
 193      // always require review.
 194      $needs_review = true;
 195  
 196      if ($needs_review) {
 197        $this->willReviewCart();
 198      } else {
 199        $this->didReviewCart();
 200      }
 201  
 202      return $this;
 203    }
 204  
 205    public function willReviewCart() {
 206      $this->openTransaction();
 207        $this->beginReadLocking();
 208  
 209          $copy = clone $this;
 210          $copy->reload();
 211  
 212          if (($copy->getStatus() !== self::STATUS_CHARGED)) {
 213            throw new Exception(
 214              pht(
 215                'Cart has wrong status ("%s") to call willReviewCart()!',
 216                $copy->getStatus()));
 217          }
 218  
 219          $this->setStatus(self::STATUS_REVIEW)->save();
 220  
 221        $this->endReadLocking();
 222      $this->saveTransaction();
 223  
 224      $this->recordCartTransaction(PhortuneCartTransaction::TYPE_REVIEW);
 225  
 226      return $this;
 227    }
 228  
 229    public function didReviewCart() {
 230      $this->openTransaction();
 231        $this->beginReadLocking();
 232  
 233          $copy = clone $this;
 234          $copy->reload();
 235  
 236          if (($copy->getStatus() !== self::STATUS_CHARGED) &&
 237              ($copy->getStatus() !== self::STATUS_REVIEW)) {
 238            throw new Exception(
 239              pht(
 240                'Cart has wrong status ("%s") to call didReviewCart()!',
 241                $copy->getStatus()));
 242          }
 243  
 244          foreach ($this->purchases as $purchase) {
 245            $purchase->getProduct()->didPurchaseProduct($purchase);
 246          }
 247  
 248          $this->setStatus(self::STATUS_PURCHASED)->save();
 249  
 250        $this->endReadLocking();
 251      $this->saveTransaction();
 252  
 253      $this->recordCartTransaction(PhortuneCartTransaction::TYPE_PURCHASED);
 254  
 255      return $this;
 256    }
 257  
 258    public function didFailCharge(PhortuneCharge $charge) {
 259      $charge->setStatus(PhortuneCharge::STATUS_FAILED);
 260  
 261      $this->openTransaction();
 262        $this->beginReadLocking();
 263  
 264          $copy = clone $this;
 265          $copy->reload();
 266  
 267          if (($copy->getStatus() !== self::STATUS_PURCHASING) &&
 268              ($copy->getStatus() !== self::STATUS_HOLD)) {
 269            throw new Exception(
 270              pht(
 271                'Cart has wrong status ("%s") to call didFailCharge().',
 272                $copy->getStatus()));
 273          }
 274  
 275          $charge->save();
 276  
 277          // Move the cart back into STATUS_READY so the user can try
 278          // making the purchase again.
 279          $this->setStatus(self::STATUS_READY)->save();
 280  
 281        $this->endReadLocking();
 282      $this->saveTransaction();
 283  
 284      return $this;
 285    }
 286  
 287  
 288    public function willRefundCharge(
 289      PhabricatorUser $actor,
 290      PhortunePaymentProvider $provider,
 291      PhortuneCharge $charge,
 292      PhortuneCurrency $amount) {
 293  
 294      if (!$amount->isPositive()) {
 295        throw new Exception(
 296          pht('Trying to refund nonpositive amount of money!'));
 297      }
 298  
 299      if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
 300        throw new Exception(
 301          pht('Trying to refund more money than remaining on charge!'));
 302      }
 303  
 304      if ($charge->getRefundedChargePHID()) {
 305        throw new Exception(
 306          pht('Trying to refund a refund!'));
 307      }
 308  
 309      if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
 310          ($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
 311        throw new Exception(
 312          pht('Trying to refund an uncharged charge!'));
 313      }
 314  
 315      $refund_charge = PhortuneCharge::initializeNewCharge()
 316        ->setAccountPHID($this->getAccount()->getPHID())
 317        ->setCartPHID($this->getPHID())
 318        ->setAuthorPHID($actor->getPHID())
 319        ->setMerchantPHID($this->getMerchant()->getPHID())
 320        ->setProviderPHID($provider->getProviderConfig()->getPHID())
 321        ->setPaymentMethodPHID($charge->getPaymentMethodPHID())
 322        ->setRefundedChargePHID($charge->getPHID())
 323        ->setAmountAsCurrency($amount->negate());
 324  
 325      $charge->openTransaction();
 326        $charge->beginReadLocking();
 327  
 328          $copy = clone $charge;
 329          $copy->reload();
 330  
 331          if ($copy->getRefundingPHID() !== null) {
 332            throw new Exception(
 333              pht('Trying to refund a charge which is already refunding!'));
 334          }
 335  
 336          $refund_charge->save();
 337          $charge->setRefundingPHID($refund_charge->getPHID());
 338          $charge->save();
 339  
 340        $charge->endReadLocking();
 341      $charge->saveTransaction();
 342  
 343      return $refund_charge;
 344    }
 345  
 346    public function didRefundCharge(
 347      PhortuneCharge $charge,
 348      PhortuneCharge $refund) {
 349  
 350      $refund->setStatus(PhortuneCharge::STATUS_CHARGED);
 351  
 352      $this->openTransaction();
 353        $this->beginReadLocking();
 354  
 355          $copy = clone $charge;
 356          $copy->reload();
 357  
 358          if ($charge->getRefundingPHID() !== $refund->getPHID()) {
 359            throw new Exception(
 360              pht('Charge is in the wrong refunding state!'));
 361          }
 362  
 363          $charge->setRefundingPHID(null);
 364  
 365          // NOTE: There's some trickiness here to get the signs right. Both
 366          // these values are positive but the refund has a negative value.
 367          $total_refunded = $charge
 368            ->getAmountRefundedAsCurrency()
 369            ->add($refund->getAmountAsCurrency()->negate());
 370  
 371          $charge->setAmountRefundedAsCurrency($total_refunded);
 372          $charge->save();
 373          $refund->save();
 374  
 375        $this->endReadLocking();
 376      $this->saveTransaction();
 377  
 378      $amount = $refund->getAmountAsCurrency()->negate();
 379      foreach ($this->purchases as $purchase) {
 380        $purchase->getProduct()->didRefundProduct($purchase, $amount);
 381      }
 382  
 383      return $this;
 384    }
 385  
 386    public function didFailRefund(
 387      PhortuneCharge $charge,
 388      PhortuneCharge $refund) {
 389  
 390      $refund->setStatus(PhortuneCharge::STATUS_FAILED);
 391  
 392      $this->openTransaction();
 393        $this->beginReadLocking();
 394  
 395          $copy = clone $charge;
 396          $copy->reload();
 397  
 398          if ($charge->getRefundingPHID() !== $refund->getPHID()) {
 399            throw new Exception(
 400              pht('Charge is in the wrong refunding state!'));
 401          }
 402  
 403          $charge->setRefundingPHID(null);
 404          $charge->save();
 405          $refund->save();
 406  
 407        $this->endReadLocking();
 408      $this->saveTransaction();
 409    }
 410  
 411    private function recordCartTransaction($type) {
 412      $omnipotent_user = PhabricatorUser::getOmnipotentUser();
 413      $phortune_phid = id(new PhabricatorPhortuneApplication())->getPHID();
 414  
 415      $xactions = array();
 416  
 417      $xactions[] = id(new PhortuneCartTransaction())
 418        ->setTransactionType($type)
 419        ->setNewValue(true);
 420  
 421      $content_source = PhabricatorContentSource::newForSource(
 422        PhabricatorContentSource::SOURCE_PHORTUNE,
 423        array());
 424  
 425      $editor = id(new PhortuneCartEditor())
 426        ->setActor($omnipotent_user)
 427        ->setActingAsPHID($phortune_phid)
 428        ->setContentSource($content_source)
 429        ->setContinueOnMissingFields(true)
 430        ->setContinueOnNoEffect(true);
 431  
 432      $editor->applyTransactions($this, $xactions);
 433    }
 434  
 435    public function getName() {
 436      return $this->getImplementation()->getName($this);
 437    }
 438  
 439    public function getDoneURI() {
 440      return $this->getImplementation()->getDoneURI($this);
 441    }
 442  
 443    public function getDoneActionName() {
 444      return $this->getImplementation()->getDoneActionName($this);
 445    }
 446  
 447    public function getCancelURI() {
 448      return $this->getImplementation()->getCancelURI($this);
 449    }
 450  
 451    public function getDetailURI() {
 452      return '/phortune/cart/'.$this->getID().'/';
 453    }
 454  
 455    public function getCheckoutURI() {
 456      return '/phortune/cart/'.$this->getID().'/checkout/';
 457    }
 458  
 459    public function canCancelOrder() {
 460      try {
 461        $this->assertCanCancelOrder();
 462        return true;
 463      } catch (Exception $ex) {
 464        return false;
 465      }
 466    }
 467  
 468    public function canRefundOrder() {
 469      try {
 470        $this->assertCanRefundOrder();
 471        return true;
 472      } catch (Exception $ex) {
 473        return false;
 474      }
 475    }
 476  
 477    public function assertCanCancelOrder() {
 478      switch ($this->getStatus()) {
 479        case self::STATUS_BUILDING:
 480          throw new Exception(
 481            pht(
 482              'This order can not be cancelled because the application has not '.
 483              'finished building it yet.'));
 484        case self::STATUS_READY:
 485          throw new Exception(
 486            pht(
 487              'This order can not be cancelled because it has not been placed.'));
 488      }
 489  
 490      return $this->getImplementation()->assertCanCancelOrder($this);
 491    }
 492  
 493    public function assertCanRefundOrder() {
 494      switch ($this->getStatus()) {
 495        case self::STATUS_BUILDING:
 496          throw new Exception(
 497            pht(
 498              'This order can not be refunded because the application has not '.
 499              'finished building it yet.'));
 500        case self::STATUS_READY:
 501          throw new Exception(
 502            pht(
 503              'This order can not be refunded because it has not been placed.'));
 504      }
 505  
 506      return $this->getImplementation()->assertCanRefundOrder($this);
 507    }
 508  
 509    public function getConfiguration() {
 510      return array(
 511        self::CONFIG_AUX_PHID => true,
 512        self::CONFIG_SERIALIZATION => array(
 513          'metadata' => self::SERIALIZATION_JSON,
 514        ),
 515        self::CONFIG_COLUMN_SCHEMA => array(
 516          'status' => 'text32',
 517          'cartClass' => 'text128',
 518          'mailKey' => 'bytes20',
 519        ),
 520        self::CONFIG_KEY_SCHEMA => array(
 521          'key_account' => array(
 522            'columns' => array('accountPHID'),
 523          ),
 524          'key_merchant' => array(
 525            'columns' => array('merchantPHID'),
 526          ),
 527        ),
 528      ) + parent::getConfiguration();
 529    }
 530  
 531    public function generatePHID() {
 532      return PhabricatorPHID::generateNewPHID(
 533        PhortuneCartPHIDType::TYPECONST);
 534    }
 535  
 536    public function save() {
 537      if (!$this->getMailKey()) {
 538        $this->setMailKey(Filesystem::readRandomCharacters(20));
 539      }
 540      return parent::save();
 541    }
 542  
 543    public function attachPurchases(array $purchases) {
 544      assert_instances_of($purchases, 'PhortunePurchase');
 545      $this->purchases = $purchases;
 546      return $this;
 547    }
 548  
 549    public function getPurchases() {
 550      return $this->assertAttached($this->purchases);
 551    }
 552  
 553    public function attachAccount(PhortuneAccount $account) {
 554      $this->account = $account;
 555      return $this;
 556    }
 557  
 558    public function getAccount() {
 559      return $this->assertAttached($this->account);
 560    }
 561  
 562    public function attachMerchant(PhortuneMerchant $merchant) {
 563      $this->merchant = $merchant;
 564      return $this;
 565    }
 566  
 567    public function getMerchant() {
 568      return $this->assertAttached($this->merchant);
 569    }
 570  
 571    public function attachImplementation(
 572      PhortuneCartImplementation $implementation) {
 573      $this->implementation = $implementation;
 574      return $this;
 575    }
 576  
 577    public function getImplementation() {
 578      return $this->assertAttached($this->implementation);
 579    }
 580  
 581    public function getTotalPriceAsCurrency() {
 582      $prices = array();
 583      foreach ($this->getPurchases() as $purchase) {
 584        $prices[] = $purchase->getTotalPriceAsCurrency();
 585      }
 586  
 587      return PhortuneCurrency::newFromList($prices);
 588    }
 589  
 590    public function setMetadataValue($key, $value) {
 591      $this->metadata[$key] = $value;
 592      return $this;
 593    }
 594  
 595    public function getMetadataValue($key, $default = null) {
 596      return idx($this->metadata, $key, $default);
 597    }
 598  
 599  
 600  /* -(  PhabricatorPolicyInterface  )----------------------------------------- */
 601  
 602  
 603    public function getCapabilities() {
 604      return array(
 605        PhabricatorPolicyCapability::CAN_VIEW,
 606        PhabricatorPolicyCapability::CAN_EDIT,
 607      );
 608    }
 609  
 610    public function getPolicy($capability) {
 611      // NOTE: Both view and edit use the account's edit policy. We punch a hole
 612      // through this for merchants, below.
 613      return $this
 614        ->getAccount()
 615        ->getPolicy(PhabricatorPolicyCapability::CAN_EDIT);
 616    }
 617  
 618    public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
 619      if ($this->getAccount()->hasAutomaticCapability($capability, $viewer)) {
 620        return true;
 621      }
 622  
 623      // If the viewer controls the merchant this order was placed with, they
 624      // can view the order.
 625      if ($capability == PhabricatorPolicyCapability::CAN_VIEW) {
 626        $can_admin = PhabricatorPolicyFilter::hasCapability(
 627          $viewer,
 628          $this->getMerchant(),
 629          PhabricatorPolicyCapability::CAN_EDIT);
 630        if ($can_admin) {
 631          return true;
 632        }
 633      }
 634  
 635      return false;
 636    }
 637  
 638    public function describeAutomaticCapability($capability) {
 639      return array(
 640        pht('Orders inherit the policies of the associated account.'),
 641        pht('The merchant you placed an order with can review and manage it.'),
 642      );
 643    }
 644  
 645  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1