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