4 * Copyright (C) 2006 Google Inc.
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
10 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 /* This is the response handler code that will be invoked every time
20 * a notification or request is sent by the Google Server
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
25 * Order processing commands can be sent automatically by placing these
26 * commands appropriately
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
37 define('GOOGLE_DEBUG_PP', 0);
38 class CRM_Core_Payment_GoogleIPN
extends CRM_Core_Payment_BaseIPN
{
41 * We only need one instance of this object. So we use the singleton
42 * pattern and cache the instance in this variable
47 static private $_singleton = NULL;
50 * mode of operation: live or test
54 protected $_mode = NULL;
56 static function retrieve($name, $type, $object, $abort = TRUE) {
57 $value = CRM_Utils_Array
::value($name, $object);
58 if ($abort && $value === NULL) {
59 CRM_Core_Error
::debug_log_message("Could not find an entry for $name");
60 echo "Failure: Missing Parameter<p>";
65 if (!CRM_Utils_Type
::validate($value, $type)) {
66 CRM_Core_Error
::debug_log_message("Could not find a valid entry for $name");
67 echo "Failure: Invalid Parameter<p>";
78 * @param string $mode the mode of operation: live or test
82 function __construct($mode, &$paymentProcessor) {
83 parent
::__construct();
86 $this->_paymentProcessor
= $paymentProcessor;
90 * The function gets called when a new order takes place.
92 * @param xml $dataRoot response send by google in xml format
93 * @param array $privateData contains the name value pair of <merchant-private-data>
98 function newOrderNotify($dataRoot, $privateData, $component) {
99 $ids = $input = $params = array();
101 $input['component'] = strtolower($component);
103 $ids['contact'] = self
::retrieve('contactID', 'Integer', $privateData, TRUE);
104 $ids['contribution'] = self
::retrieve('contributionID', 'Integer', $privateData, TRUE);
106 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
107 if ($input['component'] == "event") {
108 $ids['event'] = self
::retrieve('eventID', 'Integer', $privateData, TRUE);
109 $ids['participant'] = self
::retrieve('participantID', 'Integer', $privateData, TRUE);
110 $ids['membership'] = NULL;
113 $ids['membership'] = self
::retrieve('membershipID', 'Integer', $privateData, FALSE);
114 $ids['related_contact'] = self
::retrieve('relatedContactID', 'Integer', $privateData, FALSE);
115 $ids['onbehalf_dupe_alert'] = self
::retrieve('onBehalfDupeAlert', 'Integer', $privateData, FALSE);
116 $ids['contributionRecur'] = self
::retrieve('contributionRecurID', 'Integer', $privateData, FALSE);
119 $paymentProcessorID = CRM_Core_DAO
::getFieldValue(
120 'CRM_Financial_DAO_PaymentProcessorType',
123 'payment_processor_type'
126 if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) {
130 $input['invoice'] = $privateData['invoiceID'];
131 $input['newInvoice'] = $dataRoot['google-order-number']['VALUE'];
133 if ($ids['contributionRecur']) {
134 if ($objects['contributionRecur']->invoice_id
== $dataRoot['serial-number']) {
135 CRM_Core_Error
::debug_log_message("The new order notification already handled: {$dataRoot['serial-number']}.");
139 $transaction = new CRM_Core_Transaction();
141 CRM_Core_Error
::debug_log_message("New order for an installment received.");
142 $recur = &$objects['contributionRecur'];
144 // fix dates that already exist
145 $dates = array('create', 'start', 'end', 'cancel', 'modified');
146 foreach ($dates as $date) {
147 $name = "{$date}_date";
149 $recur->$name = CRM_Utils_Date
::isoToMysql($recur->$name);
152 $recur->invoice_id
= $dataRoot['serial-number'];
153 $recur->processor_id
= $input['newInvoice'];
156 if ($objects['contribution']->contribution_status_id
== 1) {
157 // create a contribution and then get it processed
158 $contribution = new CRM_Contribute_DAO_Contribution();
159 $contribution->contact_id
= $ids['contact'];
160 $contribution->financial_type_id
= $objects['contributionType']->id
;
161 $contribution->contribution_page_id
= $objects['contribution']->contribution_page_id
;
162 $contribution->contribution_recur_id
= $ids['contributionRecur'];
163 $contribution->receive_date
= date('YmdHis');
164 $contribution->currency
= $objects['contribution']->currency
;
165 $contribution->payment_instrument_id
= $objects['contribution']->payment_instrument_id
;
166 $contribution->amount_level
= $objects['contribution']->amount_level
;
167 $contribution->address_id
= $objects['contribution']->address_id
;
168 $contribution->invoice_id
= $input['invoice'];
169 $contribution->total_amount
= $dataRoot['order-total']['VALUE'];
170 $contribution->contribution_status_id
= 2;
171 $contribution->honor_contact_id
= $objects['contribution']->honor_contact_id
;
172 $contribution->honor_type_id
= $objects['contribution']->honor_type_id
;
173 $contribution->campaign_id
= $objects['contribution']->campaign_id
;
175 $objects['contribution'] = $contribution;
177 $transaction->commit();
181 // make sure the invoice is valid and matches what we have in the contribution record
182 $contribution = &$objects['contribution'];
184 if ($contribution->invoice_id
!= $input['invoice']) {
185 CRM_Core_Error
::debug_log_message("Invoice values dont match between database and IPN request");
189 // lets replace invoice-id with google-order-number because thats what is common and unique
190 // in subsequent calls or notifications sent by google.
191 $contribution->invoice_id
= $input['newInvoice'];
193 $input['amount'] = $dataRoot['order-total']['VALUE'];
195 if ($contribution->total_amount
!= $input['amount']) {
196 CRM_Core_Error
::debug_log_message("Amount values dont match between database and IPN request");
200 if (!$this->getInput($input, $ids, $dataRoot)) {
204 $transaction = new CRM_Core_Transaction();
206 // check if contribution is already completed, if so we ignore this ipn
207 if ($contribution->contribution_status_id
== 1) {
208 CRM_Core_Error
::debug_log_message("returning since contribution has already been handled");
212 /* Since trxn_id hasn't got any use here,
213 * lets make use of it by passing the eventID/membershipTypeID to next level.
214 * And change trxn_id to google-order-number before finishing db update */
216 if (CRM_Utils_Array
::value('event', $ids)) {
217 $contribution->trxn_id
= $ids['event'] . CRM_Core_DAO
::VALUE_SEPARATOR
. $ids['participant'];
219 elseif (CRM_Utils_Array
::value('membership', $ids)) {
220 $contribution->trxn_id
= $ids['membership'][0] . CRM_Core_DAO
::VALUE_SEPARATOR
. $ids['related_contact'] . CRM_Core_DAO
::VALUE_SEPARATOR
. $ids['onbehalf_dupe_alert'];
224 // CRM_Core_Error::debug_var( 'c', $contribution );
225 $contribution->save();
226 $transaction->commit();
232 * The function gets called when the state(CHARGED, CANCELLED..) changes for an order
234 * @param string $status status of the transaction send by google
235 * @param array $privateData contains the name value pair of <merchant-private-data>
240 function orderStateChange($status, $dataRoot, $privateData, $component) {
241 $input = $objects = $ids = array();
242 $input['component'] = strtolower($component);
244 $ids['contributionRecur'] = self
::retrieve('contributionRecurID', 'Integer', $privateData, FALSE);
245 $serial = $dataRoot['serial-number'];
246 $orderNo = $dataRoot['google-order-number']['VALUE'];
248 $contribution = new CRM_Contribute_BAO_Contribution();
249 $contribution->invoice_id
= $orderNo;
251 if (!$contribution->find(TRUE)) {
252 CRM_Core_Error
::debug_log_message("orderStateChange: Could not find contribution record with invoice id: $serial");
256 // Google sends the charged notification twice.
257 // So to make sure, code is not executed again.
258 if ($contribution->contribution_status_id
== 1) {
259 CRM_Core_Error
::debug_log_message("Contribution already handled (ContributionID = {$contribution->id}).");
263 // make sure invoice is set to serial no for recurring payments, to avoid violating uniqueness
264 $contribution->invoice_id
= $ids['contributionRecur'] ?
$serial : $orderNo;
266 $objects['contribution'] = &$contribution;
267 $ids['contribution'] = $contribution->id
;
268 $ids['contact'] = $contribution->contact_id
;
270 $ids['event'] = $ids['participant'] = $ids['membership'] = NULL;
271 $ids['contributionPage'] = NULL;
273 if ($input['component'] == "event") {
274 list($ids['event'], $ids['participant']) = explode(CRM_Core_DAO
::VALUE_SEPARATOR
,
275 $contribution->trxn_id
279 $ids['related_contact'] = NULL;
280 $ids['onbehalf_dupe_alert'] = NULL;
281 if ($contribution->trxn_id
) {
282 list($ids['membership'], $ids['related_contact'], $ids['onbehalf_dupe_alert']) = explode(CRM_Core_DAO
::VALUE_SEPARATOR
,
283 $contribution->trxn_id
287 'membership', 'related_contact', 'onbehalf_dupe_alert') as $fld) {
288 if (!is_numeric($ids[$fld])) {
294 $paymentProcessorID = CRM_Core_DAO
::getFieldValue(
295 'CRM_Financial_DAO_PaymentProcessorType',
298 'payment_processor_type'
301 $this->loadObjects($input, $ids, $objects, TRUE, $paymentProcessorID);
303 $transaction = new CRM_Core_Transaction();
305 // CRM_Core_Error::debug_var( 'c', $contribution );
306 if ($status == 'PAYMENT_DECLINED' ||
307 $status == 'CANCELLED_BY_GOOGLE' ||
308 $status == 'CANCELLED'
310 return $this->failed($objects, $transaction);
313 $input['amount'] = $contribution->total_amount
;
314 $input['fee_amount'] = NULL;
315 $input['net_amount'] = NULL;
316 $input['trxn_id'] = $ids['contributionRecur'] ?
$serial : $dataRoot['google-order-number']['VALUE'];
317 $input['is_test'] = $contribution->is_test
;
320 if ($ids['contributionRecur']) {
321 $recur = $objects['contributionRecur'];
323 $this->completeTransaction($input, $ids, $objects, $transaction, $recur);
325 $this->completeRecur($input, $ids, $objects);
328 function completeRecur($input, $ids, $objects) {
329 if ($ids['contributionRecur']) {
330 $recur = &$objects['contributionRecur'];
331 $contributionCount = CRM_Core_DAO
::singleValueQuery("
333 FROM civicrm_contribution
334 WHERE contribution_recur_id = {$ids['contributionRecur']}
336 $autoRenewMembership = FALSE;
338 isset($ids['membership']) &&
341 $autoRenewMembership = TRUE;
343 if ($recur->installments
&& ($contributionCount >= $recur->installments
)) {
344 $contributionStatus = CRM_Contribute_PseudoConstant
::contributionStatus(NULL, 'name');
346 $recur->create_date
= CRM_Utils_Date
::isoToMysql($recur->create_date
);
347 $recur->start_date
= CRM_Utils_Date
::isoToMysql($recur->start_date
);
348 $recur->cancel_date
= CRM_Utils_Date
::isoToMysql($recur->cancel_date
);
349 $recur->end_date
= date('YmdHis');
350 $recur->modified_date
= date('YmdHis');
351 $recur->contribution_status_id
= array_search('Completed', $contributionStatus);
352 $recur->trnx_id
= $dataRoot['google-order-number']['VALUE'];
355 //send recurring Notification email for user
356 CRM_Contribute_BAO_ContributionPage
::recurringNotify(
357 CRM_Core_Payment
::RECURRING_PAYMENT_END
,
359 $ids['contributionPage'],
364 elseif ($contributionCount == 1) {
365 CRM_Contribute_BAO_ContributionPage
::recurringNotify(
366 CRM_Core_Payment
::RECURRING_PAYMENT_START
,
368 $ids['contributionPage'],
377 * singleton function used to manage this object
379 * @param string $mode the mode of operation: live or test
384 static function &singleton($mode, $component, &$paymentProcessor) {
385 if (self
::$_singleton === NULL) {
386 self
::$_singleton = new CRM_Core_Payment_GoogleIPN($mode, $paymentProcessor);
388 return self
::$_singleton;
392 * The function retrieves the amount the contribution is for, based on the order-no google sends
394 * @param int $orderNo <order-total> send by google
399 function getAmount($orderNo) {
400 $contribution = new CRM_Contribute_DAO_Contribution();
401 $contribution->invoice_id
= $orderNo;
402 if (!$contribution->find(TRUE)) {
403 CRM_Core_Error
::debug_log_message("getAmount: Could not find contribution record with invoice id: $orderNo");
404 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
407 return $contribution->total_amount
;
411 * The function returns the component(Event/Contribute..), given the google-order-no and merchant-private-data
413 * @param xml $xml_response response send by google in xml format
414 * @param array $privateData contains the name value pair of <merchant-private-data>
415 * @param int $orderNo <order-total> send by google
416 * @param string $root root of xml-response
418 * @return array context of this call (test, module, payment processor id)
421 function getContext($privateData, $orderNo, $root, $response, $serial) {
422 $contributionID = CRM_Utils_Array
::value('contributionID', $privateData);
423 $contribution = new CRM_Contribute_DAO_Contribution();
424 if ($root == 'new-order-notification') {
425 $contribution->id
= $contributionID;
428 $contribution->invoice_id
= $orderNo;
430 if (!$contribution->find(TRUE)) {
431 CRM_Core_Error
::debug_log_message("getContext: Could not find contribution record with invoice id: $orderNo");
432 $response->SendAck($serial);
435 $module = 'Contribute';
436 if (stristr($contribution->source
, ts('Online Contribution'))) {
437 $module = 'Contribute';
439 elseif (stristr($contribution->source
, ts('Online Event Registration'))) {
442 $isTest = $contribution->is_test
;
444 $ids = $input = $objects = array();
445 $objects['contribution'] = &$contribution;
446 $ids['contributionRecur'] = self
::retrieve('contributionRecurID', 'Integer', $privateData, FALSE);
447 $input['component'] = strtolower($module);
449 if (!$ids['contributionRecur'] && $contribution->contribution_status_id
== 1) {
450 CRM_Core_Error
::debug_log_message("Contribution already handled (ContributionID = {$contribution->id}).");
451 // There is no point in going further. Return ack so we don't receive the same ipn.
452 $response->SendAck($serial);
455 if ($input['component'] == 'event') {
456 if ($root == 'new-order-notification') {
457 $ids['event'] = $privateData['eventID'];
460 list($ids['event'], $ids['participant']) =
461 explode(CRM_Core_DAO
::VALUE_SEPARATOR
, $contribution->trxn_id
);
465 $paymentProcessorID = CRM_Core_DAO
::getFieldValue(
466 'CRM_Financial_DAO_PaymentProcessor',
469 'payment_processor_type'
472 $this->loadObjects($input, $ids, $objects, FALSE, $paymentProcessorID);
474 if (!$ids['paymentProcessor']) {
475 CRM_Core_Error
::debug_log_message("Payment processor could not be retrieved.");
476 // There is no point in going further. Return ack so we don't receive the same ipn.
477 $response->SendAck($serial);
480 return array($isTest, $input['component'], $ids['paymentProcessor']);
484 * This method is handles the response that will be invoked (from extern/googleNotify) every time
485 * a notification or request is sent by the Google Server.
488 static function main($xml_response) {
489 require_once 'Google/library/googleresponse.php';
490 require_once 'Google/library/googlerequest.php';
491 require_once 'Google/library/googlemerchantcalculations.php';
492 require_once 'Google/library/googleresult.php';
493 require_once 'Google/library/xml-processing/gc_xmlparser.php';
495 $config = CRM_Core_Config
::singleton();
497 // Retrieve the XML sent in the HTTP POST request to the ResponseHandler
498 if (get_magic_quotes_gpc()) {
499 $xml_response = stripslashes($xml_response);
502 $headers = CRM_Utils_System
::getAllHeaders();
504 if (GOOGLE_DEBUG_PP
) {
505 CRM_Core_Error
::debug_var('RESPONSE', $xml_response, TRUE, TRUE, 'Google');
508 // Retrieve the root and data from the xml response
509 $response = new GoogleResponse();
510 list($root, $data) = $response->GetParsedXML($xml_response);
511 // lets retrieve the private-data & order-no
513 if (array_key_exists('shopping-cart', $data[$root])) {
514 $privateData = $data[$root]['shopping-cart']['merchant-private-data']['VALUE'];
516 if (empty($privateData) && array_key_exists('order-summary', $data[$root])
517 && array_key_exists('shopping-cart', $data[$root]['order-summary'])) {
518 $privateData = $data[$root]['order-summary']['shopping-cart']['merchant-private-data']['VALUE'];
520 $privateData = $privateData ? self
::stringToArray($privateData) : '';
521 $orderNo = $data[$root]['google-order-number']['VALUE'];
522 $serial = $data[$root]['serial-number'];
524 // a dummy object to call get context and a parent function inside it.
525 $ipn = new CRM_Core_Payment_GoogleIPN('live', $dummyProcessor);
526 list($mode, $module, $paymentProcessorID) = $ipn->getContext($privateData, $orderNo, $root, $response, $serial);
527 $mode = $mode ?
'test' : 'live';
529 $paymentProcessor = CRM_Financial_BAO_PaymentProcessor
::getPayment($paymentProcessorID, $mode);
530 $merchant_id = $paymentProcessor['user_name'];
531 $merchant_key = $paymentProcessor['password'];
532 $response->SetMerchantAuthentication($merchant_id, $merchant_key);
534 $server_type = ($mode == 'test') ?
'sandbox' : 'production';
535 $request = new GoogleRequest($merchant_id, $merchant_key, $server_type);
537 $ipn = self
::singleton($mode, $module, $paymentProcessor);
539 if (GOOGLE_DEBUG_PP
) {
540 CRM_Core_Error
::debug_var('RESPONSE-ROOT', $response->root
, TRUE, TRUE, 'Google');
543 //Check status and take appropriate action
544 $status = $response->HttpAuthentication($headers);
547 case "request-received":
550 case "checkout-redirect":
551 case "merchant-calculation-callback":
554 case "new-order-notification": {
555 $response->SendAck($serial, FALSE);
556 $ipn->newOrderNotify($data[$root], $privateData, $module);
560 case "order-state-change-notification": {
561 $response->SendAck($serial, FALSE);
562 $new_financial_state = $data[$root]['new-financial-order-state']['VALUE'];
563 $new_fulfillment_order = $data[$root]['new-fulfillment-order-state']['VALUE'];
565 switch ($new_financial_state) {
570 case 'PAYMENT_DECLINED':
572 case 'CANCELLED_BY_GOOGLE':
573 $ipn->orderStateChange($new_financial_state, $data[$root], $privateData, $module);
586 case "authorization-amount-notification": {
587 $response->SendAck($serial, FALSE);
588 $new_financial_state = $data[$root]['order-summary']['financial-order-state']['VALUE'];
589 $new_fulfillment_order = $data[$root]['order-summary']['fulfillment-order-state']['VALUE'];
591 switch ($new_financial_state) {
593 // For google-handled subscriptions chargeorder needn't be initiated,
594 // assuming auto-charging is turned on.
595 //$request->SendProcessOrder($data[$root]['google-order-number']['VALUE']);
596 //$request->SendChargeOrder($data[$root]['google-order-number']['VALUE'],'');
600 case 'PAYMENT_DECLINED':
606 case 'CANCELLED_BY_GOOGLE':
615 case "charge-amount-notification":
616 case "chargeback-amount-notification":
617 case "refund-amount-notification":
618 case "risk-information-notification":
619 $response->SendAck($serial);
627 function getInput(&$input, &$ids, $dataRoot) {
628 if (!$this->getBillingID($ids)) {
632 $billingID = $ids['billing'];
634 "first_name" => 'contact-name',
635 // "last-name" not available with google (every thing in contact-name)
636 "last_name" => 'last_name',
637 "street_address-{$billingID}" => 'address1',
638 "city-{$billingID}" => 'city',
639 "state-{$billingID}" => 'region',
640 "postal_code-{$billingID}" => 'postal-code',
641 "country-{$billingID}" => 'country-code',
644 foreach ($lookup as $name => $googleName) {
645 if (array_key_exists($googleName, $dataRoot['buyer-billing-address'])) {
646 $value = $dataRoot['buyer-billing-address'][$googleName]['VALUE'];
648 $input[$name] = $value ?
$value : NULL;
654 * Converts the comma separated name-value pairs in <merchant-private-data>
655 * to an array of name-value pairs.
657 static function stringToArray($str) {
658 $vars = $labels = array();
659 $labels = explode(',', $str);
660 foreach ($labels as $label) {
661 $terms = explode('=', $label);
662 $vars[$terms[0]] = $terms[1];