3 final class PhortunePayPalPaymentProvider
extends PhortunePaymentProvider
{
5 const PAYPAL_API_USERNAME
= 'paypal.api-username';
6 const PAYPAL_API_PASSWORD
= 'paypal.api-password';
7 const PAYPAL_API_SIGNATURE
= 'paypal.api-signature';
8 const PAYPAL_MODE
= 'paypal.mode';
10 public function isAcceptingLivePayments() {
11 $mode = $this->getProviderConfig()->getMetadataValue(self
::PAYPAL_MODE
);
12 return ($mode === 'live');
15 public function getName() {
19 public function getConfigureName() {
20 return pht('Add PayPal Payments Account');
23 public function getConfigureDescription() {
25 'Allows you to accept various payment instruments with a paypal.com '.
29 public function getConfigureProvidesDescription() {
30 return pht('This merchant accepts payments via PayPal.');
33 public function getConfigureInstructions() {
35 "To configure PayPal, register or log into an existing account on ".
36 "[[https://paypal.com | paypal.com]] (for live payments) or ".
37 "[[https://sandbox.paypal.com | sandbox.paypal.com]] (for test ".
38 "payments). Once logged in:\n\n".
39 " - Navigate to {nav Tools > API Access}.\n".
40 " - Choose **View API Signature**.\n".
41 " - Copy the **API Username**, **API Password** and **Signature** ".
42 " into the fields above.\n\n".
43 "You can select whether the provider operates in test mode or ".
44 "accepts live payments using the **Mode** dropdown above.\n\n".
45 "You can either use `sandbox.paypal.com` to retrieve live credentials, ".
46 "or `paypal.com` to retrieve live credentials.");
49 public function getAllConfigurableProperties() {
51 self
::PAYPAL_API_USERNAME
,
52 self
::PAYPAL_API_PASSWORD
,
53 self
::PAYPAL_API_SIGNATURE
,
58 public function getAllConfigurableSecretProperties() {
60 self
::PAYPAL_API_PASSWORD
,
61 self
::PAYPAL_API_SIGNATURE
,
65 public function processEditForm(
66 AphrontRequest
$request,
72 if (!strlen($values[self
::PAYPAL_API_USERNAME
])) {
73 $errors[] = pht('PayPal API Username is required.');
74 $issues[self
::PAYPAL_API_USERNAME
] = pht('Required');
77 if (!strlen($values[self
::PAYPAL_API_PASSWORD
])) {
78 $errors[] = pht('PayPal API Password is required.');
79 $issues[self
::PAYPAL_API_PASSWORD
] = pht('Required');
82 if (!strlen($values[self
::PAYPAL_API_SIGNATURE
])) {
83 $errors[] = pht('PayPal API Signature is required.');
84 $issues[self
::PAYPAL_API_SIGNATURE
] = pht('Required');
87 if (!strlen($values[self
::PAYPAL_MODE
])) {
88 $errors[] = pht('Mode is required.');
89 $issues[self
::PAYPAL_MODE
] = pht('Required');
92 return array($errors, $issues, $values);
95 public function extendEditForm(
96 AphrontRequest
$request,
97 AphrontFormView
$form,
103 id(new AphrontFormTextControl())
104 ->setName(self
::PAYPAL_API_USERNAME
)
105 ->setValue($values[self
::PAYPAL_API_USERNAME
])
106 ->setError(idx($issues, self
::PAYPAL_API_USERNAME
, true))
107 ->setLabel(pht('Paypal API Username')))
109 id(new AphrontFormTextControl())
110 ->setName(self
::PAYPAL_API_PASSWORD
)
111 ->setValue($values[self
::PAYPAL_API_PASSWORD
])
112 ->setError(idx($issues, self
::PAYPAL_API_PASSWORD
, true))
113 ->setLabel(pht('Paypal API Password')))
115 id(new AphrontFormTextControl())
116 ->setName(self
::PAYPAL_API_SIGNATURE
)
117 ->setValue($values[self
::PAYPAL_API_SIGNATURE
])
118 ->setError(idx($issues, self
::PAYPAL_API_SIGNATURE
, true))
119 ->setLabel(pht('Paypal API Signature')))
121 id(new AphrontFormSelectControl())
122 ->setName(self
::PAYPAL_MODE
)
123 ->setValue($values[self
::PAYPAL_MODE
])
124 ->setError(idx($issues, self
::PAYPAL_MODE
))
125 ->setLabel(pht('Mode'))
128 'test' => pht('Test Mode'),
129 'live' => pht('Live Mode'),
135 public function canRunConfigurationTest() {
139 public function runConfigurationTest() {
142 ->setRawPayPalQuery('GetBalance', array())
146 public function getPaymentMethodDescription() {
147 return pht('Credit Card or PayPal Account');
150 public function getPaymentMethodIcon() {
154 public function getPaymentMethodProviderDescription() {
158 protected function executeCharge(
159 PhortunePaymentMethod
$payment_method,
160 PhortuneCharge
$charge) {
161 throw new Exception('!');
164 protected function executeRefund(
165 PhortuneCharge
$charge,
166 PhortuneCharge
$refund) {
168 $transaction_id = $charge->getMetadataValue('paypal.transactionID');
169 if (!$transaction_id) {
170 throw new Exception(pht('Charge has no transaction ID!'));
173 $refund_amount = $refund->getAmountAsCurrency()->negate();
174 $refund_currency = $refund_amount->getCurrency();
175 $refund_value = $refund_amount->formatBareValue();
178 'TRANSACTIONID' => $transaction_id,
179 'REFUNDTYPE' => 'Partial',
180 'AMT' => $refund_value,
181 'CURRENCYCODE' => $refund_currency,
186 ->setRawPayPalQuery('RefundTransaction', $params)
189 $charge->setMetadataValue(
191 $result['REFUNDTRANSACTIONID']);
194 public function updateCharge(PhortuneCharge
$charge) {
195 $transaction_id = $charge->getMetadataValue('paypal.transactionID');
196 if (!$transaction_id) {
197 throw new Exception(pht('Charge has no transaction ID!'));
201 'TRANSACTIONID' => $transaction_id,
206 ->setRawPayPalQuery('GetTransactionDetails', $params)
211 switch ($result['PAYMENTSTATUS']) {
214 case 'Completed-Funds-Held':
217 case 'Partially-Refunded':
220 case 'Canceled-Reversal':
221 // TODO: Handle these.
225 // TODO: Also handle these better?
237 if ($charge->getStatus() == PhortuneCharge
::STATUS_HOLD
) {
238 $cart = $charge->getCart();
240 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
242 $cart->didApplyCharge($charge);
243 } else if ($is_fail) {
244 $cart->didFailCharge($charge);
250 private function getPaypalAPIUsername() {
252 ->getProviderConfig()
253 ->getMetadataValue(self
::PAYPAL_API_USERNAME
);
256 private function getPaypalAPIPassword() {
258 ->getProviderConfig()
259 ->getMetadataValue(self
::PAYPAL_API_PASSWORD
);
262 private function getPaypalAPISignature() {
264 ->getProviderConfig()
265 ->getMetadataValue(self
::PAYPAL_API_SIGNATURE
);
268 /* -( One-Time Payments )-------------------------------------------------- */
270 public function canProcessOneTimePayments() {
274 /* -( Controllers )-------------------------------------------------------- */
277 public function canRespondToControllerAction($action) {
284 return parent
::canRespondToControllerAction();
287 public function processControllerRequest(
288 PhortuneProviderActionController
$controller,
289 AphrontRequest
$request) {
291 $viewer = $request->getUser();
293 $cart = $controller->loadCart($request->getInt('cartID'));
295 return new Aphront404Response();
298 $charge = $controller->loadActiveCharge($cart);
299 switch ($controller->getAction()) {
302 throw new Exception(pht('Cart is already charging!'));
308 throw new Exception(pht('Cart is not charging yet!'));
313 switch ($controller->getAction()) {
315 $return_uri = $this->getControllerURI(
318 'cartID' => $cart->getID(),
321 $cancel_uri = $this->getControllerURI(
324 'cartID' => $cart->getID(),
327 $price = $cart->getTotalPriceAsCurrency();
329 $charge = $cart->willApplyCharge($viewer, $this);
332 'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
333 'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
334 'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
335 'PAYMENTREQUEST_0_CUSTOM' => $charge->getPHID(),
336 'PAYMENTREQUEST_0_DESC' => $cart->getName(),
338 'RETURNURL' => $return_uri,
339 'CANCELURL' => $cancel_uri,
341 // TODO: This should be cart-dependent if we eventually support
348 ->setRawPayPalQuery('SetExpressCheckout', $params)
352 'cmd' => '_express-checkout',
353 'token' => $result['TOKEN'],
356 $uri = new PhutilURI(
357 'https://www.sandbox.paypal.com/cgi-bin/webscr',
360 $cart->setMetadataValue('provider.checkoutURI', (string)$uri);
363 $charge->setMetadataValue('paypal.token', $result['TOKEN']);
366 return id(new AphrontRedirectResponse())
367 ->setIsExternal(true)
370 if ($cart->getStatus() !== PhortuneCart
::STATUS_PURCHASING
) {
371 return id(new AphrontRedirectResponse())
372 ->setURI($cart->getCheckoutURI());
375 $token = $request->getStr('token');
383 ->setRawPayPalQuery('GetExpressCheckoutDetails', $params)
386 if ($result['CUSTOM'] !== $charge->getPHID()) {
388 pht('Paypal checkout does not match Phortune charge!'));
391 if ($result['CHECKOUTSTATUS'] !== 'PaymentActionNotInitiated') {
392 return $controller->newDialog()
393 ->setTitle(pht('Payment Already Processed'))
396 'The payment response for this charge attempt has already '.
398 ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
401 $price = $cart->getTotalPriceAsCurrency();
405 'PAYERID' => $result['PAYERID'],
407 'PAYMENTREQUEST_0_AMT' => $price->formatBareValue(),
408 'PAYMENTREQUEST_0_CURRENCYCODE' => $price->getCurrency(),
409 'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
414 ->setRawPayPalQuery('DoExpressCheckoutPayment', $params)
417 $transaction_id = $result['PAYMENTINFO_0_TRANSACTIONID'];
421 switch ($result['PAYMENTINFO_0_PAYMENTSTATUS']) {
424 case 'Completed-Funds-Held':
429 // TODO: We can capture more information about this stuff.
435 case 'Partially-Refunded':
436 case 'Canceled-Reversal':
442 // These are all failure states.
446 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
448 $charge->setMetadataValue('paypal.transactionID', $transaction_id);
452 $cart->didApplyCharge($charge);
453 $response = id(new AphrontRedirectResponse())->setURI(
454 $cart->getCheckoutURI());
456 $cart->didHoldCharge($charge);
458 $response = $controller
460 ->setTitle(pht('Charge On Hold'))
462 pht('Your charge is on hold, for reasons?'))
463 ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
465 $cart->didFailCharge($charge);
467 $response = $controller
469 ->setTitle(pht('Charge Failed'))
470 ->addCancelButton($cart->getCheckoutURI(), pht('Continue'));
476 if ($cart->getStatus() === PhortuneCart
::STATUS_PURCHASING
) {
477 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
478 // TODO: Since the user cancelled this, we could conceivably just
479 // throw it away or make it more clear that it's a user cancel.
480 $cart->didFailCharge($charge);
484 return id(new AphrontRedirectResponse())
485 ->setURI($cart->getCheckoutURI());
489 pht('Unsupported action "%s".', $controller->getAction()));
492 private function newPaypalAPICall() {
493 if ($this->isAcceptingLivePayments()) {
494 $host = 'https://api-3t.paypal.com/nvp';
496 $host = 'https://api-3t.sandbox.paypal.com/nvp';
499 return id(new PhutilPayPalAPIFuture())
501 ->setAPIUsername($this->getPaypalAPIUsername())
502 ->setAPIPassword($this->getPaypalAPIPassword())
503 ->setAPISignature($this->getPaypalAPISignature());