[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |