1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25:
26:
27: 28: 29:
30: class Mage_Paypal_Model_Ipn
31: {
32: 33: 34: 35: 36:
37: const DEFAULT_LOG_FILE = 'paypal_unknown_ipn.log';
38:
39: 40: 41:
42: protected $_order = null;
43:
44: 45: 46: 47: 48:
49: protected $_recurringProfile = null;
50:
51: 52: 53: 54:
55: protected $_config = null;
56:
57: 58: 59: 60: 61:
62: protected $_info = null;
63:
64: 65: 66: 67:
68: protected $_request = array();
69:
70: 71: 72: 73: 74:
75: protected $_debugData = array();
76:
77: 78: 79: 80: 81: 82:
83: public function getRequestData($key = null)
84: {
85: if (null === $key) {
86: return $this->_request;
87: }
88: return isset($this->_request[$key]) ? $this->_request[$key] : null;
89: }
90:
91: 92: 93: 94: 95: 96: 97:
98: public function processIpnRequest(array $request, Zend_Http_Client_Adapter_Interface $httpAdapter = null)
99: {
100: $this->_request = $request;
101: $this->_debugData = array('ipn' => $request);
102: ksort($this->_debugData['ipn']);
103:
104: try {
105: if (isset($this->_request['txn_type']) && 'recurring_payment' == $this->_request['txn_type']) {
106: $this->_getRecurringProfile();
107: if ($httpAdapter) {
108: $this->_postBack($httpAdapter);
109: }
110: $this->_processRecurringProfile();
111: } else {
112: $this->_getOrder();
113: if ($httpAdapter) {
114: $this->_postBack($httpAdapter);
115: }
116: $this->_processOrder();
117: }
118: } catch (Exception $e) {
119: $this->_debugData['exception'] = $e->getMessage();
120: $this->_debug();
121: throw $e;
122: }
123: $this->_debug();
124: }
125:
126: 127: 128: 129: 130:
131: protected function _postBack(Zend_Http_Client_Adapter_Interface $httpAdapter)
132: {
133: $sReq = '';
134: foreach ($this->_request as $k => $v) {
135: $sReq .= '&'.$k.'='.urlencode($v);
136: }
137: $sReq .= "&cmd=_notify-validate";
138: $sReq = substr($sReq, 1);
139: $this->_debugData['postback'] = $sReq;
140: $this->_debugData['postback_to'] = $this->_config->getPaypalUrl();
141:
142: $httpAdapter->setConfig(array('verifypeer' => $this->_config->verifyPeer));
143: $httpAdapter->write(Zend_Http_Client::POST, $this->_config->getPaypalUrl(), '1.1', array(), $sReq);
144: try {
145: $response = $httpAdapter->read();
146: } catch (Exception $e) {
147: $this->_debugData['http_error'] = array('error' => $e->getMessage(), 'code' => $e->getCode());
148: throw $e;
149: }
150: $this->_debugData['postback_result'] = $response;
151:
152: $response = preg_split('/^\r?$/m', $response, 2);
153: $response = trim($response[1]);
154: if ($response != 'VERIFIED') {
155: throw new Exception('PayPal IPN postback failure. See ' . self::DEFAULT_LOG_FILE . ' for details.');
156: }
157: unset($this->_debugData['postback'], $this->_debugData['postback_result']);
158: }
159:
160: 161: 162: 163: 164: 165: 166:
167: protected function _getOrder()
168: {
169: if (empty($this->_order)) {
170:
171: $id = $this->_request['invoice'];
172: $this->_order = Mage::getModel('sales/order')->loadByIncrementId($id);
173: if (!$this->_order->getId()) {
174: $this->_debugData['exception'] = sprintf('Wrong order ID: "%s".', $id);
175: $this->_debug();
176: Mage::app()->getResponse()
177: ->setHeader('HTTP/1.1','503 Service Unavailable')
178: ->sendResponse();
179: exit;
180: }
181:
182: $methodCode = $this->_order->getPayment()->getMethod();
183: $this->_config = Mage::getModel('paypal/config', array($methodCode, $this->_order->getStoreId()));
184: if (!$this->_config->isMethodActive($methodCode) || !$this->_config->isMethodAvailable()) {
185: throw new Exception(sprintf('Method "%s" is not available.', $methodCode));
186: }
187:
188: $this->_verifyOrder();
189: }
190: return $this->_order;
191: }
192:
193: 194: 195: 196: 197: 198:
199: protected function _getRecurringProfile()
200: {
201: if (empty($this->_recurringProfile)) {
202:
203: $internalReferenceId = $this->_request['rp_invoice_id'];
204: $this->_recurringProfile = Mage::getModel('sales/recurring_profile')
205: ->loadByInternalReferenceId($internalReferenceId);
206: if (!$this->_recurringProfile->getId()) {
207: throw new Exception(
208: sprintf('Wrong recurring profile INTERNAL_REFERENCE_ID: "%s".', $internalReferenceId)
209: );
210: }
211:
212: $methodCode = $this->_recurringProfile->getMethodCode();
213: $this->_config = Mage::getModel(
214: 'paypal/config', array($methodCode, $this->_recurringProfile->getStoreId())
215: );
216: if (!$this->_config->isMethodActive($methodCode) || !$this->_config->isMethodAvailable()) {
217: throw new Exception(sprintf('Method "%s" is not available.', $methodCode));
218: }
219: }
220: return $this->_recurringProfile;
221: }
222:
223: 224: 225: 226: 227: 228:
229: protected function _verifyOrder()
230: {
231:
232: $merchantEmail = $this->_config->businessAccount;
233: if ($merchantEmail) {
234: $receiverEmail = $this->getRequestData('business');
235: if (!$receiverEmail) {
236: $receiverEmail = $this->getRequestData('receiver_email');
237: }
238: if (strtolower($merchantEmail) != strtolower($receiverEmail)) {
239: throw new Exception(
240: sprintf(
241: 'Requested %s and configured %s merchant emails do not match.', $receiverEmail, $merchantEmail
242: )
243: );
244: }
245: }
246: }
247:
248: 249: 250: 251: 252:
253: protected function _processOrder()
254: {
255: $this->_order = null;
256: $this->_getOrder();
257:
258: $this->_info = Mage::getSingleton('paypal/info');
259: try {
260:
261: $paymentStatus = $this->_filterPaymentStatus($this->_request['payment_status']);
262:
263: switch ($paymentStatus) {
264:
265: case Mage_Paypal_Model_Info::PAYMENTSTATUS_COMPLETED:
266: $this->_registerPaymentCapture();
267: break;
268:
269:
270: case Mage_Paypal_Model_Info::PAYMENTSTATUS_DENIED:
271: $this->_registerPaymentDenial();
272: break;
273:
274:
275: case Mage_Paypal_Model_Info::PAYMENTSTATUS_FAILED:
276:
277: $this->_registerPaymentFailure();
278: break;
279:
280:
281: case Mage_Paypal_Model_Info::PAYMENTSTATUS_REVERSED:
282: case Mage_Paypal_Model_Info::PAYMENTSTATUS_UNREVERSED:
283: $this->_registerPaymentReversal();
284: break;
285:
286:
287: case Mage_Paypal_Model_Info::PAYMENTSTATUS_REFUNDED:
288: $this->_registerPaymentRefund();
289: break;
290:
291:
292: case Mage_Paypal_Model_Info::PAYMENTSTATUS_PENDING:
293: $this->_registerPaymentPending();
294: break;
295:
296:
297: case Mage_Paypal_Model_Info::PAYMENTSTATUS_PROCESSED:
298: $this->_registerMasspaymentsSuccess();
299: break;
300:
301:
302: case Mage_Paypal_Model_Info::PAYMENTSTATUS_EXPIRED:
303: case Mage_Paypal_Model_Info::PAYMENTSTATUS_VOIDED:
304: $this->_registerPaymentVoid();
305: break;
306:
307: default:
308: throw new Exception("Cannot handle payment status '{$paymentStatus}'.");
309: }
310: } catch (Mage_Core_Exception $e) {
311: $comment = $this->_createIpnComment(Mage::helper('paypal')->__('Note: %s', $e->getMessage()), true);
312: $comment->save();
313: throw $e;
314: }
315: }
316:
317: 318: 319:
320: protected function _processRecurringProfile()
321: {
322: $this->_recurringProfile = null;
323: $this->_getRecurringProfile();
324:
325: try {
326:
327: $paymentStatus = $this->_filterPaymentStatus($this->_request['payment_status']);
328:
329: switch ($paymentStatus) {
330:
331: case Mage_Paypal_Model_Info::PAYMENTSTATUS_COMPLETED:
332: $this->_registerRecurringProfilePaymentCapture();
333: break;
334:
335: default:
336: throw new Exception("Cannot handle payment status '{$paymentStatus}'.");
337: }
338: } catch (Mage_Core_Exception $e) {
339:
340:
341:
342: throw $e;
343: }
344: }
345:
346: 347: 348:
349: protected function _registerRecurringProfilePaymentCapture()
350: {
351: $price = $this->getRequestData('mc_gross') - $this->getRequestData('tax') - $this->getRequestData('shipping');
352: $productItemInfo = new Varien_Object;
353: $type = trim($this->getRequestData('period_type'));
354: if ($type == 'Trial') {
355: $productItemInfo->setPaymentType(Mage_Sales_Model_Recurring_Profile::PAYMENT_TYPE_TRIAL);
356: } elseif ($type == 'Regular') {
357: $productItemInfo->setPaymentType(Mage_Sales_Model_Recurring_Profile::PAYMENT_TYPE_REGULAR);
358: }
359: $productItemInfo->setTaxAmount($this->getRequestData('tax'));
360: $productItemInfo->setShippingAmount($this->getRequestData('shipping'));
361: $productItemInfo->setPrice($price);
362:
363: $order = $this->_recurringProfile->createOrder($productItemInfo);
364:
365: $payment = $order->getPayment();
366: $payment->setTransactionId($this->getRequestData('txn_id'))
367: ->setPreparedMessage($this->_createIpnComment(''))
368: ->setIsTransactionClosed(0);
369: $order->save();
370: $this->_recurringProfile->addOrderRelation($order->getId());
371: $payment->registerCaptureNotification($this->getRequestData('mc_gross'));
372: $order->save();
373:
374:
375: if ($invoice = $payment->getCreatedInvoice()) {
376: $message = Mage::helper('paypal')->__('Notified customer about invoice #%s.', $invoice->getIncrementId());
377: $comment = $order->sendNewOrderEmail()->addStatusHistoryComment($message)
378: ->setIsCustomerNotified(true)
379: ->save();
380: }
381: }
382:
383: 384: 385:
386: protected function _registerPaymentCapture()
387: {
388: if ($this->getRequestData('transaction_entity') == 'auth') {
389: return;
390: }
391: $this->_importPaymentInformation();
392: $payment = $this->_order->getPayment();
393: $payment->setTransactionId($this->getRequestData('txn_id'))
394: ->setPreparedMessage($this->_createIpnComment(''))
395: ->setParentTransactionId($this->getRequestData('parent_txn_id'))
396: ->setShouldCloseParentTransaction('Completed' === $this->getRequestData('auth_status'))
397: ->setIsTransactionClosed(0)
398: ->registerCaptureNotification($this->getRequestData('mc_gross'));
399: $this->_order->save();
400:
401:
402: $invoice = $payment->getCreatedInvoice();
403: if ($invoice && !$this->_order->getEmailSent()) {
404: $this->_order->sendNewOrderEmail()->addStatusHistoryComment(
405: Mage::helper('paypal')->__('Notified customer about invoice #%s.', $invoice->getIncrementId())
406: )
407: ->setIsCustomerNotified(true)
408: ->save();
409: }
410: }
411:
412: 413: 414:
415: protected function _registerPaymentDenial()
416: {
417: $this->_importPaymentInformation();
418: $this->_order->getPayment()
419: ->setTransactionId($this->getRequestData('txn_id'))
420: ->setNotificationResult(true)
421: ->setIsTransactionClosed(true)
422: ->registerPaymentReviewAction(Mage_Sales_Model_Order_Payment::REVIEW_ACTION_DENY, false);
423: $this->_order->save();
424: }
425:
426: 427: 428:
429: protected function _registerPaymentFailure()
430: {
431: $this->_importPaymentInformation();
432: $this->_order
433: ->registerCancellation($this->_createIpnComment(''), false)
434: ->save();
435: }
436:
437: 438: 439:
440: protected function _registerPaymentRefund()
441: {
442: $this->_importPaymentInformation();
443: $reason = $this->getRequestData('reason_code');
444: $isRefundFinal = !$this->_info->isReversalDisputable($reason);
445: $payment = $this->_order->getPayment()
446: ->setPreparedMessage($this->_createIpnComment($this->_info->explainReasonCode($reason)))
447: ->setTransactionId($this->getRequestData('txn_id'))
448: ->setParentTransactionId($this->getRequestData('parent_txn_id'))
449: ->setIsTransactionClosed($isRefundFinal)
450: ->registerRefundNotification(-1 * $this->getRequestData('mc_gross'));
451: $this->_order->save();
452:
453:
454:
455: if ($creditmemo = $payment->getCreatedCreditmemo()) {
456: $creditmemo->sendEmail();
457: $comment = $this->_order->addStatusHistoryComment(
458: Mage::helper('paypal')->__('Notified customer about creditmemo #%s.', $creditmemo->getIncrementId())
459: )
460: ->setIsCustomerNotified(true)
461: ->save();
462: }
463: }
464:
465: 466: 467:
468: protected function _registerPaymentReversal()
469: {
470: 471: 472: 473:
474: if ($this->_info->isPaymentReviewRequired($this->_order->getPayment())) {
475: $this->_registerPaymentDenial();
476: return;
477: }
478:
479: if ('chargeback_reimbursement' == $this->getRequestData('reason_code')) {
480:
481: return;
482: }
483:
484:
485: $this->_registerPaymentRefund();
486: }
487:
488: 489: 490: 491: 492:
493: public function _registerPaymentPending()
494: {
495: $reason = $this->getRequestData('pending_reason');
496: if ('authorization' === $reason) {
497: $this->_registerPaymentAuthorization();
498: return;
499: }
500: if ('order' === $reason) {
501: throw new Exception('The "order" authorizations are not implemented.');
502: }
503:
504:
505: if (Mage_Sales_Model_Order::STATE_PENDING_PAYMENT == $this->_order->getState()) {
506: $this->_registerPaymentCapture();
507: return;
508: }
509:
510: $this->_importPaymentInformation();
511:
512: $this->_order->getPayment()
513: ->setPreparedMessage($this->_createIpnComment($this->_info->explainPendingReason($reason)))
514: ->setTransactionId($this->getRequestData('txn_id'))
515: ->setIsTransactionClosed(0)
516: ->registerPaymentReviewAction(Mage_Sales_Model_Order_Payment::REVIEW_ACTION_UPDATE, false);
517: $this->_order->save();
518: }
519:
520: 521: 522:
523: protected function _registerPaymentAuthorization()
524: {
525: $this->_importPaymentInformation();
526:
527: $this->_order->getPayment()
528: ->setPreparedMessage($this->_createIpnComment(''))
529: ->setTransactionId($this->getRequestData('txn_id'))
530: ->setParentTransactionId($this->getRequestData('parent_txn_id'))
531: ->setIsTransactionClosed(0)
532: ->registerAuthorizationNotification($this->getRequestData('mc_gross'));
533: if (!$this->_order->getEmailSent()) {
534: $this->_order->sendNewOrderEmail();
535: }
536: $this->_order->save();
537: }
538:
539: 540: 541:
542: protected function _registerPaymentVoid()
543: {
544: $this->_importPaymentInformation();
545:
546: $parentTxnId = $this->getRequestData('transaction_entity') == 'auth'
547: ? $this->getRequestData('txn_id') : $this->getRequestData('parent_txn_id');
548:
549: $this->_order->getPayment()
550: ->setPreparedMessage($this->_createIpnComment(''))
551: ->setParentTransactionId($parentTxnId)
552: ->registerVoidNotification();
553:
554: $this->_order->save();
555: }
556:
557: 558: 559: 560:
561: protected function _registerMasspaymentsSuccess()
562: {
563: $comment = $this->_createIpnComment('', true);
564: $comment->save();
565: }
566:
567: 568: 569: 570: 571: 572: 573: 574:
575: protected function ($comment = '', $addToHistory = false)
576: {
577: $paymentStatus = $this->getRequestData('payment_status');
578: $message = Mage::helper('paypal')->__('IPN "%s".', $paymentStatus);
579: if ($comment) {
580: $message .= ' ' . $comment;
581: }
582: if ($addToHistory) {
583: $message = $this->_order->addStatusHistoryComment($message);
584: $message->setIsCustomerNotified(null);
585: }
586: return $message;
587: }
588:
589: 590: 591: 592: 593: 594: 595:
596: protected function _importPaymentInformation()
597: {
598: $payment = $this->_order->getPayment();
599: $was = $payment->getAdditionalInformation();
600:
601:
602: $from = array();
603: foreach (array(
604: Mage_Paypal_Model_Info::PAYER_ID,
605: 'payer_email' => Mage_Paypal_Model_Info::PAYER_EMAIL,
606: Mage_Paypal_Model_Info::PAYER_STATUS,
607: Mage_Paypal_Model_Info::ADDRESS_STATUS,
608: Mage_Paypal_Model_Info::PROTECTION_EL,
609: Mage_Paypal_Model_Info::PAYMENT_STATUS,
610: Mage_Paypal_Model_Info::PENDING_REASON,
611: ) as $privateKey => $publicKey) {
612: if (is_int($privateKey)) {
613: $privateKey = $publicKey;
614: }
615: $value = $this->getRequestData($privateKey);
616: if ($value) {
617: $from[$publicKey] = $value;
618: }
619: }
620: if (isset($from['payment_status'])) {
621: $from['payment_status'] = $this->_filterPaymentStatus($this->getRequestData('payment_status'));
622: }
623:
624:
625: $fraudFilters = array();
626: for ($i = 1; $value = $this->getRequestData("fraud_management_pending_filters_{$i}"); $i++) {
627: $fraudFilters[] = $value;
628: }
629: if ($fraudFilters) {
630: $from[Mage_Paypal_Model_Info::FRAUD_FILTERS] = $fraudFilters;
631: }
632:
633: $this->_info->importToPayment($from, $payment);
634:
635: 636: 637: 638: 639:
640: if ($this->_info->isPaymentReviewRequired($payment)) {
641: $payment->setIsTransactionPending(true);
642: if ($fraudFilters) {
643: $payment->setIsFraudDetected(true);
644: }
645: }
646: if ($this->_info->isPaymentSuccessful($payment)) {
647: $payment->setIsTransactionApproved(true);
648: } elseif ($this->_info->isPaymentFailed($payment)) {
649: $payment->setIsTransactionDenied(true);
650: }
651:
652: return $was != $payment->getAdditionalInformation();
653: }
654:
655: 656: 657: 658: 659: 660:
661: protected function _filterPaymentStatus($ipnPaymentStatus)
662: {
663: switch ($ipnPaymentStatus) {
664: case 'Created':
665: case 'Completed': return Mage_Paypal_Model_Info::PAYMENTSTATUS_COMPLETED;
666: case 'Denied': return Mage_Paypal_Model_Info::PAYMENTSTATUS_DENIED;
667: case 'Expired': return Mage_Paypal_Model_Info::PAYMENTSTATUS_EXPIRED;
668: case 'Failed': return Mage_Paypal_Model_Info::PAYMENTSTATUS_FAILED;
669: case 'Pending': return Mage_Paypal_Model_Info::PAYMENTSTATUS_PENDING;
670: case 'Refunded': return Mage_Paypal_Model_Info::PAYMENTSTATUS_REFUNDED;
671: case 'Reversed': return Mage_Paypal_Model_Info::PAYMENTSTATUS_REVERSED;
672: case 'Canceled_Reversal': return Mage_Paypal_Model_Info::PAYMENTSTATUS_UNREVERSED;
673: case 'Processed': return Mage_Paypal_Model_Info::PAYMENTSTATUS_PROCESSED;
674: case 'Voided': return Mage_Paypal_Model_Info::PAYMENTSTATUS_VOIDED;
675: }
676: return '';
677:
678:
679:
680:
681: }
682:
683: 684: 685: 686: 687:
688: protected function _debug()
689: {
690: if ($this->_config && $this->_config->debug) {
691: $file = $this->_config->getMethodCode() ? "payment_{$this->_config->getMethodCode()}.log"
692: : self::DEFAULT_LOG_FILE;
693: Mage::getModel('core/log_adapter', $file)->log($this->_debugData);
694: }
695: }
696: }
697: