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