Merge branch 'phpunit-ob-fix' of https://github.com/giant-rabbit/civicrm-core into...
[civicrm-core.git] / tools / extensions / org.civicrm.payment.googlecheckout / GoogleIPN.php
CommitLineData
6a488035
TO
1<?php
2
3/**
4 * Copyright (C) 2006 Google Inc.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18
19/* This is the response handler code that will be invoked every time
20 * a notification or request is sent by the Google Server
21 *
22 * To allow this code to receive responses, the url for this file
23 * must be set on the seller page under Settings->Integration as the
24 * "API Callback URL'
25 * Order processing commands can be sent automatically by placing these
26 * commands appropriately
27 *
28 * To use this code for merchant-calculated feedback, this url must be
29 * set also as the merchant-calculations-url when the cart is posted
30 * Depending on your calculations for shipping, taxes, coupons and gift
31 * certificates update parts of the code as required
32 *
33 */
34
35
36
37require_once 'CRM/Core/Payment/BaseIPN.php';
38
39define('GOOGLE_DEBUG_PP', 1);
a1a55b61
EM
40
41/**
42 * Class org_civicrm_payment_googlecheckout_GoogleIPN
43 */
6a488035
TO
44class org_civicrm_payment_googlecheckout_GoogleIPN extends CRM_Core_Payment_BaseIPN {
45
46 /**
47 * We only need one instance of this object. So we use the singleton
48 * pattern and cache the instance in this variable
49 *
50 * @var object
51 * @static
52 */
53 static private $_singleton = NULL;
54
55 /**
56 * mode of operation: live or test
57 *
58 * @var object
59 * @static
60 */
61 static protected $_mode = NULL;
62
a1a55b61
EM
63 /**
64 * @param $name
65 * @param $type
66 * @param $object
67 * @param bool $abort
68 *
69 * @return mixed
70 */
6a488035
TO
71 static function retrieve($name, $type, $object, $abort = TRUE) {
72 $value = CRM_Utils_Array::value($name, $object);
73 if ($abort && $value === NULL) {
74 CRM_Core_Error::debug_log_message("Could not find an entry for $name");
75 echo "Failure: Missing Parameter<p>";
76 exit();
77 }
78
79 if ($value) {
80 if (!CRM_Utils_Type::validate($value, $type)) {
81 CRM_Core_Error::debug_log_message("Could not find a valid entry for $name");
82 echo "Failure: Invalid Parameter<p>";
83 exit();
84 }
85 }
86
87 return $value;
88 }
89
90 /**
91 * Constructor
92 *
93 * @param string $mode the mode of operation: live or test
94 *
2a6da8d7
EM
95 * @param $paymentProcessor
96 *
97 * @return \org_civicrm_payment_googlecheckout_GoogleIPN
6a488035
TO
98 */
99 function __construct($mode, &$paymentProcessor) {
100 parent::__construct();
101
102 $this->_mode = $mode;
103 $this->_paymentProcessor = $paymentProcessor;
104 }
105
106 /**
107 * The function gets called when a new order takes place.
108 *
2a6da8d7 109 * @param xml $dataRoot response send by google in xml format
6a488035
TO
110 * @param array $privateData contains the name value pair of <merchant-private-data>
111 *
2a6da8d7 112 * @param $component
6a488035 113 * @return void
6a488035
TO
114 */
115 function newOrderNotify($dataRoot, $privateData, $component) {
116 $ids = $input = $params = array();
117
118 $input['component'] = strtolower($component);
119
120 $ids['contact'] = self::retrieve('contactID', 'Integer', $privateData, TRUE);
121 $ids['contribution'] = self::retrieve('contributionID', 'Integer', $privateData, TRUE);
122
123 if ($input['component'] == "event") {
124 $ids['event'] = self::retrieve('eventID', 'Integer', $privateData, TRUE);
125 $ids['participant'] = self::retrieve('participantID', 'Integer', $privateData, TRUE);
126 $ids['membership'] = NULL;
127 }
128 else {
129 $ids['membership'] = self::retrieve('membershipID', 'Integer', $privateData, FALSE);
130 $ids['related_contact'] = self::retrieve('relatedContactID', 'Integer', $privateData, FALSE);
131 $ids['onbehalf_dupe_alert'] = self::retrieve('onBehalfDupeAlert', 'Integer', $privateData, FALSE);
132 }
133 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
134
135 if (!$this->validateData($input, $ids, $objects)) {
136 return FALSE;
137 }
138
139 // make sure the invoice is valid and matches what we have in the contribution record
140 $input['invoice'] = $privateData['invoiceID'];
141 $input['newInvoice'] = $dataRoot['google-order-number']['VALUE'];
142 $contribution = &$objects['contribution'];
143 if ($contribution->invoice_id != $input['invoice']) {
144 CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request");
145 echo "Failure: Invoice values dont match between database and IPN request<p>";
146 return;
147 }
148
149 // lets replace invoice-id with google-order-number because thats what is common and unique
150 // in subsequent calls or notifications sent by google.
151 $contribution->invoice_id = $input['newInvoice'];
152
153 $input['amount'] = $dataRoot['order-total']['VALUE'];
154
155 if ($contribution->total_amount != $input['amount']) {
156 CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request");
157 echo "Failure: Amount values dont match between database and IPN request<p>";
158 return;
159 }
160
161 if (!$this->getInput($input, $ids)) {
162 return FALSE;
163 }
164
165 require_once 'CRM/Core/Transaction.php';
166 $transaction = new CRM_Core_Transaction();
167
6a488035
TO
168 // check if contribution is already completed, if so we ignore this ipn
169 if ($contribution->contribution_status_id == 1) {
170 CRM_Core_Error::debug_log_message("returning since contribution has already been handled");
171 echo "Success: Contribution has already been handled<p>";
172 }
173 else {
1a9e3953 174 /* Since trxn_id hasn't got any use here,
6a488035
TO
175 * lets make use of it by passing the eventID/membershipTypeID to next level.
176 * And change trxn_id to google-order-number before finishing db update */
177
178
179 if ($ids['event']) {
180 $contribution->trxn_id = $ids['event'] . CRM_Core_DAO::VALUE_SEPARATOR . $ids['participant'];
181 }
182 else {
183 $contribution->trxn_id = $ids['membership'] . CRM_Core_DAO::VALUE_SEPARATOR . $ids['related_contact'] . CRM_Core_DAO::VALUE_SEPARATOR . $ids['onbehalf_dupe_alert'];
184 }
185 }
186
6a488035
TO
187 $contribution->save();
188 $transaction->commit();
189 return TRUE;
190 }
191
192 /**
193 * The function gets called when the state(CHARGED, CANCELLED..) changes for an order
194 *
2a6da8d7
EM
195 * @param string $status status of the transaction send by google
196 * @param $dataRoot
197 * @param $component
198 * @internal param array $privateData contains the name value pair of <merchant-private-data>
6a488035
TO
199 *
200 * @return void
6a488035
TO
201 */
202 function orderStateChange($status, $dataRoot, $component) {
203 $input = $objects = $ids = array();
204
205 $input['component'] = strtolower($component);
206
6a488035
TO
207 $orderNo = $dataRoot['google-order-number']['VALUE'];
208
209 require_once 'CRM/Contribute/DAO/Contribution.php';
210 $contribution = new CRM_Contribute_DAO_Contribution();
211 $contribution->invoice_id = $orderNo;
212 if (!$contribution->find(TRUE)) {
213 CRM_Core_Error::debug_log_message("Could not find contribution record with invoice id: $orderNo");
214 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
215 exit();
216 }
217
218 // Google sends the charged notification twice.
219 // So to make sure, code is not executed again.
220 if ($contribution->contribution_status_id == 1) {
221 CRM_Core_Error::debug_log_message("Contribution already handled (ContributionID = $contribution).");
222 exit();
223 }
224
225 $objects['contribution'] = &$contribution;
226 $ids['contribution'] = $contribution->id;
227 $ids['contact'] = $contribution->contact_id;
228
229 $ids['event'] = $ids['participant'] = $ids['membership'] = NULL;
230 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
231
232 if ($input['component'] == "event") {
233 list($ids['event'], $ids['participant']) = explode(CRM_Core_DAO::VALUE_SEPARATOR,
234 $contribution->trxn_id
235 );
236 }
237 else {
238 list($ids['membership'], $ids['related_contact'], $ids['onbehalf_dupe_alert']) = explode(CRM_Core_DAO::VALUE_SEPARATOR,
239 $contribution->trxn_id
240 );
241
242 foreach (array('membership', 'related_contact', 'onbehalf_dupe_alert') as $fld) {
243 if (!is_numeric($ids[$fld])) {
244 unset($ids[$fld]);
245 }
246 }
247 }
248
249 $this->loadObjects($input, $ids, $objects);
250
251 require_once 'CRM/Core/Transaction.php';
252 $transaction = new CRM_Core_Transaction();
253
6a488035
TO
254 if ($status == 'PAYMENT_DECLINED' ||
255 $status == 'CANCELLED_BY_GOOGLE' ||
256 $status == 'CANCELLED'
257 ) {
258 return $this->failed($objects, $transaction);
259 }
260
261 $input['amount'] = $contribution->total_amount;
262 $input['fee_amount'] = NULL;
263 $input['net_amount'] = NULL;
264 $input['trxn_id'] = $orderNo;
265 $input['is_test'] = $contribution->is_test;
266
267 $this->completeTransaction($input, $ids, $objects, $transaction);
268 }
269
270 /**
271 * singleton function used to manage this object
272 *
273 * @param string $mode the mode of operation: live or test
274 *
2a6da8d7
EM
275 * @param $component
276 * @param $paymentProcessor
6a488035
TO
277 * @return object
278 * @static
279 */
280 static
281 function &singleton($mode, $component, &$paymentProcessor) {
282 if (self::$_singleton === NULL) {
283 self::$_singleton = new org_civicrm_payment_googlecheckout_GoogleIPN($mode, $paymentProcessor);
284 }
285 return self::$_singleton;
286 }
287
288 /**
289 * The function retrieves the amount the contribution is for, based on the order-no google sends
290 *
291 * @param int $orderNo <order-total> send by google
292 *
293 * @return amount
294 * @access public
295 */
296 function getAmount($orderNo) {
297 require_once 'CRM/Contribute/DAO/Contribution.php';
298 $contribution = new CRM_Contribute_DAO_Contribution();
299 $contribution->invoice_id = $orderNo;
300 if (!$contribution->find(TRUE)) {
301 CRM_Core_Error::debug_log_message("Could not find contribution record with invoice id: $orderNo");
302 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
303 exit();
304 }
305 return $contribution->total_amount;
306 }
307
308 /**
309 * The function returns the component(Event/Contribute..), given the google-order-no and merchant-private-data
310 *
311 * @param xml $xml_response response send by google in xml format
312 * @param array $privateData contains the name value pair of <merchant-private-data>
313 * @param int $orderNo <order-total> send by google
314 * @param string $root root of xml-response
315 *
316 * @return array context of this call (test, module, payment processor id)
317 * @static
318 */
319 static
320 function getContext($xml_response, $privateData, $orderNo, $root) {
321 require_once 'CRM/Contribute/DAO/Contribution.php';
322
323 $isTest = NULL;
324 $module = NULL;
325 if ($root == 'new-order-notification') {
326 $contributionID = $privateData['contributionID'];
327 $contribution = new CRM_Contribute_DAO_Contribution();
328 $contribution->id = $contributionID;
329 if (!$contribution->find(TRUE)) {
330 CRM_Core_Error::debug_log_message("Could not find contribution record: $contributionID");
331 echo "Failure: Could not find contribution record for $contributionID<p>";
332 exit();
333 }
334 if (stristr($contribution->source, ts('Online Contribution'))) {
335 $module = 'Contribute';
336 }
337 elseif (stristr($contribution->source, ts('Online Event Registration'))) {
338 $module = 'Event';
339 }
340 $isTest = $contribution->is_test;
341 }
342 else {
343 $contribution = new CRM_Contribute_DAO_Contribution();
344 $contribution->invoice_id = $orderNo;
345 if (!$contribution->find(TRUE)) {
346 CRM_Core_Error::debug_log_message("Could not find contribution record with invoice id: $orderNo");
347 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
348 exit();
349 }
350 if (stristr($contribution->source, ts('Online Contribution'))) {
351 $module = 'Contribute';
352 }
353 elseif (stristr($contribution->source, ts('Online Event Registration'))) {
354 $module = 'Event';
355 }
356 $isTest = $contribution->is_test;
357 }
358
359 if ($contribution->contribution_status_id == 1) {
360 //contribution already handled.
361 exit();
362 }
363
364 if ($module == 'Contribute') {
365 if (!$contribution->contribution_page_id) {
366 CRM_Core_Error::debug_log_message("Could not find contribution page for contribution record: $contributionID");
367 echo "Failure: Could not find contribution page for contribution record: $contributionID<p>";
368 exit();
369 }
370
371 // get the payment processor id from contribution page
372 $paymentProcessorID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionPage',
373 $contribution->contribution_page_id,
374 'payment_processor_id'
375 );
376 }
377 else {
378 if ($root == 'new-order-notification') {
379 $eventID = $privateData['eventID'];
380 }
381 else {
382 list($eventID, $participantID) = explode(CRM_Core_DAO::VALUE_SEPARATOR,
383 $contribution->trxn_id
384 );
385 }
386 if (!$eventID) {
387 CRM_Core_Error::debug_log_message("Could not find event ID");
388 echo "Failure: Could not find eventID<p>";
389 exit();
390 }
391
392 // we are in event mode
393 // make sure event exists and is valid
394 require_once 'CRM/Event/DAO/Event.php';
395 $event = new CRM_Event_DAO_Event();
396 $event->id = $eventID;
397 if (!$event->find(TRUE)) {
398 CRM_Core_Error::debug_log_message("Could not find event: $eventID");
399 echo "Failure: Could not find event: $eventID<p>";
400 exit();
401 }
402
403 // get the payment processor id from contribution page
404 $paymentProcessorID = $event->payment_processor_id;
405 }
406
407 if (!$paymentProcessorID) {
408 CRM_Core_Error::debug_log_message("Could not find payment processor for contribution record: $contributionID");
409 echo "Failure: Could not find payment processor for contribution record: $contributionID<p>";
410 exit();
411 }
412
413 return array($isTest, $module, $paymentProcessorID);
414 }
415
416 /**
417 * This method is handles the response that will be invoked (from extern/googleNotify) every time
418 * a notification or request is sent by the Google Server.
419 *
420 */
421 static
422 function main($xml_response) {
423 require_once ('Google/library/googleresponse.php');
424 require_once ('Google/library/googlemerchantcalculations.php');
425 require_once ('Google/library/googleresult.php');
426 require_once ('Google/library/xml-processing/xmlparser.php');
427
428 $config = CRM_Core_Config::singleton();
429
430 // Retrieve the XML sent in the HTTP POST request to the ResponseHandler
431 if (get_magic_quotes_gpc()) {
432 $xml_response = stripslashes($xml_response);
433 }
434
435 require_once 'CRM/Utils/System.php';
436 $headers = CRM_Utils_System::getAllHeaders();
437
438 if (GOOGLE_DEBUG_PP) {
439 CRM_Core_Error::debug_var('RESPONSE', $xml_response, TRUE, TRUE, 'Google');
440 }
441
442 // Retrieve the root and data from the xml response
443 $xmlParser = new XmlParser($xml_response);
444 $root = $xmlParser->GetRoot();
445 $data = $xmlParser->GetData();
446
447 $orderNo = $data[$root]['google-order-number']['VALUE'];
448
449 // lets retrieve the private-data
450 $privateData = $data[$root]['shopping-cart']['merchant-private-data']['VALUE'];
451 $privateData = $privateData ? self::stringToArray($privateData) : '';
452
453 list($mode, $module, $paymentProcessorID) = self::getContext($xml_response, $privateData, $orderNo, $root);
454 $mode = $mode ? 'test' : 'live';
455
456 require_once 'CRM/Financial/BAO/PaymentProcessor.php';
457 $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($paymentProcessorID,
458 $mode
459 );
460
461 $ipn = &self::singleton($mode, $module, $paymentProcessor);
462
463 // Create new response object
464 $merchant_id = $paymentProcessor['user_name'];
465 $merchant_key = $paymentProcessor['password'];
466 $server_type = ($mode == 'test') ? "sandbox" : '';
467
468 $response = new GoogleResponse($merchant_id, $merchant_key,
469 $xml_response, $server_type
470 );
471 if (GOOGLE_DEBUG_PP) {
472 CRM_Core_Error::debug_var('RESPONSE-ROOT', $response->root, TRUE, TRUE, 'Google');
473 }
474
475 //Check status and take appropriate action
476 $status = $response->HttpAuthentication($headers);
477
478 switch ($root) {
479 case "request-received":
480 case "error":
481 case "diagnosis":
482 case "checkout-redirect":
483 case "merchant-calculation-callback":
484 break;
485
486 case "new-order-notification": {
487 $response->SendAck();
488 $ipn->newOrderNotify($data[$root], $privateData, $module);
489 break;
490 }
491 case "order-state-change-notification": {
492 $response->SendAck();
493 $new_financial_state = $data[$root]['new-financial-order-state']['VALUE'];
494 $new_fulfillment_order = $data[$root]['new-fulfillment-order-state']['VALUE'];
495
496 switch ($new_financial_state) {
497 case 'CHARGEABLE':
498 $amount = $ipn->getAmount($orderNo);
499 if ($amount) {
500 $response->SendChargeOrder($data[$root]['google-order-number']['VALUE'],
501 $amount, $message_log
502 );
503 $response->SendProcessOrder($data[$root]['google-order-number']['VALUE'],
504 $message_log
505 );
506 }
507 break;
508
509 case 'CHARGED':
510 case 'PAYMENT_DECLINED':
511 case 'CANCELLED':
512 $ipn->orderStateChange($new_financial_state, $data[$root], $module);
513 break;
514
515 case 'REVIEWING':
516 case 'CHARGING':
517 case 'CANCELLED_BY_GOOGLE':
518 break;
519
520 default:
521 break;
522 }
523 }
524 case "charge-amount-notification":
525 case "chargeback-amount-notification":
526 case "refund-amount-notification":
527 case "risk-information-notification":
528 $response->SendAck();
529 break;
530
531 default:
532 break;
533 }
534 }
535
a1a55b61
EM
536 /**
537 * @param $input
538 * @param $ids
539 *
540 * @return bool
541 */
542 function getInput(&$input, &$ids) {
6a488035
TO
543 if (!$this->getBillingID($ids)) {
544 return FALSE;
545 }
546
547 $billingID = $ids['billing'];
548 $lookup = array("first_name" => 'contact-name',
549 // "last-name" not available with google (every thing in contact-name)
550 "last_name" => 'last_name',
551 "street_address-{$billingID}" => 'address1',
552 "city-{$billingID}" => 'city',
553 "state-{$billingID}" => 'region',
554 "postal_code-{$billingID}" => 'postal-code',
555 "country-{$billingID}" => 'country-code',
556 );
557
558 foreach ($lookup as $name => $googleName) {
559 $value = $dataRoot['buyer-billing-address'][$googleName]['VALUE'];
560 $input[$name] = $value ? $value : NULL;
561 }
562 return TRUE;
563 }
564
565 /**
566 * Converts the comma separated name-value pairs in <merchant-private-data>
567 * to an array of name-value pairs.
568 */
569 static
570 function stringToArray($str) {
571 $vars = $labels = array();
572 $labels = explode(',', $str);
573 foreach ($labels as $label) {
574 $terms = explode('=', $label);
575 $vars[$terms[0]] = $terms[1];
576 }
577 return $vars;
578 }
579 }
580