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 require_once 'CRM/Core/Payment/BaseIPN.php';
39 define('GOOGLE_DEBUG_PP', 1);
42 * Class org_civicrm_payment_googlecheckout_GoogleIPN
44 class org_civicrm_payment_googlecheckout_GoogleIPN
extends CRM_Core_Payment_BaseIPN
{
47 * We only need one instance of this object. So we use the singleton
48 * pattern and cache the instance in this variable
53 static private $_singleton = NULL;
56 * mode of operation: live or test
61 static protected $_mode = NULL;
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>";
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>";
93 * @param string $mode the mode of operation: live or test
95 * @param $paymentProcessor
97 * @return \org_civicrm_payment_googlecheckout_GoogleIPN
99 function __construct($mode, &$paymentProcessor) {
100 parent
::__construct();
102 $this->_mode
= $mode;
103 $this->_paymentProcessor
= $paymentProcessor;
107 * The function gets called when a new order takes place.
109 * @param xml $dataRoot response send by google in xml format
110 * @param array $privateData contains the name value pair of <merchant-private-data>
115 function newOrderNotify($dataRoot, $privateData, $component) {
116 $ids = $input = $params = array();
118 $input['component'] = strtolower($component);
120 $ids['contact'] = self
::retrieve('contactID', 'Integer', $privateData, TRUE);
121 $ids['contribution'] = self
::retrieve('contributionID', 'Integer', $privateData, TRUE);
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;
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);
133 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
135 if (!$this->validateData($input, $ids, $objects)) {
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>";
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'];
153 $input['amount'] = $dataRoot['order-total']['VALUE'];
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>";
161 if (!$this->getInput($input, $ids)) {
165 require_once 'CRM/Core/Transaction.php';
166 $transaction = new CRM_Core_Transaction();
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>";
174 /* Since trxn_id hasn't got any use here,
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 */
180 $contribution->trxn_id
= $ids['event'] . CRM_Core_DAO
::VALUE_SEPARATOR
. $ids['participant'];
183 $contribution->trxn_id
= $ids['membership'] . CRM_Core_DAO
::VALUE_SEPARATOR
. $ids['related_contact'] . CRM_Core_DAO
::VALUE_SEPARATOR
. $ids['onbehalf_dupe_alert'];
187 // CRM_Core_Error::debug_var( 'c', $contribution );
188 $contribution->save();
189 $transaction->commit();
194 * The function gets called when the state(CHARGED, CANCELLED..) changes for an order
196 * @param string $status status of the transaction send by google
199 * @internal param array $privateData contains the name value pair of <merchant-private-data>
203 function orderStateChange($status, $dataRoot, $component) {
204 $input = $objects = $ids = array();
206 $input['component'] = strtolower($component);
208 // CRM_Core_Error::debug_var( "$status, $component", $dataRoot );
209 $orderNo = $dataRoot['google-order-number']['VALUE'];
211 require_once 'CRM/Contribute/DAO/Contribution.php';
212 $contribution = new CRM_Contribute_DAO_Contribution();
213 $contribution->invoice_id
= $orderNo;
214 if (!$contribution->find(TRUE)) {
215 CRM_Core_Error
::debug_log_message("Could not find contribution record with invoice id: $orderNo");
216 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
220 // Google sends the charged notification twice.
221 // So to make sure, code is not executed again.
222 if ($contribution->contribution_status_id
== 1) {
223 CRM_Core_Error
::debug_log_message("Contribution already handled (ContributionID = $contribution).");
227 $objects['contribution'] = &$contribution;
228 $ids['contribution'] = $contribution->id
;
229 $ids['contact'] = $contribution->contact_id
;
231 $ids['event'] = $ids['participant'] = $ids['membership'] = NULL;
232 $ids['contributionRecur'] = $ids['contributionPage'] = NULL;
234 if ($input['component'] == "event") {
235 list($ids['event'], $ids['participant']) = explode(CRM_Core_DAO
::VALUE_SEPARATOR
,
236 $contribution->trxn_id
240 list($ids['membership'], $ids['related_contact'], $ids['onbehalf_dupe_alert']) = explode(CRM_Core_DAO
::VALUE_SEPARATOR
,
241 $contribution->trxn_id
244 foreach (array('membership', 'related_contact', 'onbehalf_dupe_alert') as $fld) {
245 if (!is_numeric($ids[$fld])) {
251 $this->loadObjects($input, $ids, $objects);
253 require_once 'CRM/Core/Transaction.php';
254 $transaction = new CRM_Core_Transaction();
256 // CRM_Core_Error::debug_var( 'c', $contribution );
257 if ($status == 'PAYMENT_DECLINED' ||
258 $status == 'CANCELLED_BY_GOOGLE' ||
259 $status == 'CANCELLED'
261 return $this->failed($objects, $transaction);
264 $input['amount'] = $contribution->total_amount
;
265 $input['fee_amount'] = NULL;
266 $input['net_amount'] = NULL;
267 $input['trxn_id'] = $orderNo;
268 $input['is_test'] = $contribution->is_test
;
270 $this->completeTransaction($input, $ids, $objects, $transaction);
274 * singleton function used to manage this object
276 * @param string $mode the mode of operation: live or test
279 * @param $paymentProcessor
284 function &singleton($mode, $component, &$paymentProcessor) {
285 if (self
::$_singleton === NULL) {
286 self
::$_singleton = new org_civicrm_payment_googlecheckout_GoogleIPN($mode, $paymentProcessor);
288 return self
::$_singleton;
292 * The function retrieves the amount the contribution is for, based on the order-no google sends
294 * @param int $orderNo <order-total> send by google
299 function getAmount($orderNo) {
300 require_once 'CRM/Contribute/DAO/Contribution.php';
301 $contribution = new CRM_Contribute_DAO_Contribution();
302 $contribution->invoice_id
= $orderNo;
303 if (!$contribution->find(TRUE)) {
304 CRM_Core_Error
::debug_log_message("Could not find contribution record with invoice id: $orderNo");
305 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
308 return $contribution->total_amount
;
312 * The function returns the component(Event/Contribute..), given the google-order-no and merchant-private-data
314 * @param xml $xml_response response send by google in xml format
315 * @param array $privateData contains the name value pair of <merchant-private-data>
316 * @param int $orderNo <order-total> send by google
317 * @param string $root root of xml-response
319 * @return array context of this call (test, module, payment processor id)
323 function getContext($xml_response, $privateData, $orderNo, $root) {
324 require_once 'CRM/Contribute/DAO/Contribution.php';
328 if ($root == 'new-order-notification') {
329 $contributionID = $privateData['contributionID'];
330 $contribution = new CRM_Contribute_DAO_Contribution();
331 $contribution->id
= $contributionID;
332 if (!$contribution->find(TRUE)) {
333 CRM_Core_Error
::debug_log_message("Could not find contribution record: $contributionID");
334 echo "Failure: Could not find contribution record for $contributionID<p>";
337 if (stristr($contribution->source
, ts('Online Contribution'))) {
338 $module = 'Contribute';
340 elseif (stristr($contribution->source
, ts('Online Event Registration'))) {
343 $isTest = $contribution->is_test
;
346 $contribution = new CRM_Contribute_DAO_Contribution();
347 $contribution->invoice_id
= $orderNo;
348 if (!$contribution->find(TRUE)) {
349 CRM_Core_Error
::debug_log_message("Could not find contribution record with invoice id: $orderNo");
350 echo "Failure: Could not find contribution record with invoice id: $orderNo <p>";
353 if (stristr($contribution->source
, ts('Online Contribution'))) {
354 $module = 'Contribute';
356 elseif (stristr($contribution->source
, ts('Online Event Registration'))) {
359 $isTest = $contribution->is_test
;
362 if ($contribution->contribution_status_id
== 1) {
363 //contribution already handled.
367 if ($module == 'Contribute') {
368 if (!$contribution->contribution_page_id
) {
369 CRM_Core_Error
::debug_log_message("Could not find contribution page for contribution record: $contributionID");
370 echo "Failure: Could not find contribution page for contribution record: $contributionID<p>";
374 // get the payment processor id from contribution page
375 $paymentProcessorID = CRM_Core_DAO
::getFieldValue('CRM_Contribute_DAO_ContributionPage',
376 $contribution->contribution_page_id
,
377 'payment_processor_id'
381 if ($root == 'new-order-notification') {
382 $eventID = $privateData['eventID'];
385 list($eventID, $participantID) = explode(CRM_Core_DAO
::VALUE_SEPARATOR
,
386 $contribution->trxn_id
390 CRM_Core_Error
::debug_log_message("Could not find event ID");
391 echo "Failure: Could not find eventID<p>";
395 // we are in event mode
396 // make sure event exists and is valid
397 require_once 'CRM/Event/DAO/Event.php';
398 $event = new CRM_Event_DAO_Event();
399 $event->id
= $eventID;
400 if (!$event->find(TRUE)) {
401 CRM_Core_Error
::debug_log_message("Could not find event: $eventID");
402 echo "Failure: Could not find event: $eventID<p>";
406 // get the payment processor id from contribution page
407 $paymentProcessorID = $event->payment_processor_id
;
410 if (!$paymentProcessorID) {
411 CRM_Core_Error
::debug_log_message("Could not find payment processor for contribution record: $contributionID");
412 echo "Failure: Could not find payment processor for contribution record: $contributionID<p>";
416 return array($isTest, $module, $paymentProcessorID);
420 * This method is handles the response that will be invoked (from extern/googleNotify) every time
421 * a notification or request is sent by the Google Server.
425 function main($xml_response) {
426 require_once ('Google/library/googleresponse.php');
427 require_once ('Google/library/googlemerchantcalculations.php');
428 require_once ('Google/library/googleresult.php');
429 require_once ('Google/library/xml-processing/xmlparser.php');
431 $config = CRM_Core_Config
::singleton();
433 // Retrieve the XML sent in the HTTP POST request to the ResponseHandler
434 if (get_magic_quotes_gpc()) {
435 $xml_response = stripslashes($xml_response);
438 require_once 'CRM/Utils/System.php';
439 $headers = CRM_Utils_System
::getAllHeaders();
441 if (GOOGLE_DEBUG_PP
) {
442 CRM_Core_Error
::debug_var('RESPONSE', $xml_response, TRUE, TRUE, 'Google');
445 // Retrieve the root and data from the xml response
446 $xmlParser = new XmlParser($xml_response);
447 $root = $xmlParser->GetRoot();
448 $data = $xmlParser->GetData();
450 $orderNo = $data[$root]['google-order-number']['VALUE'];
452 // lets retrieve the private-data
453 $privateData = $data[$root]['shopping-cart']['merchant-private-data']['VALUE'];
454 $privateData = $privateData ? self
::stringToArray($privateData) : '';
456 list($mode, $module, $paymentProcessorID) = self
::getContext($xml_response, $privateData, $orderNo, $root);
457 $mode = $mode ?
'test' : 'live';
459 require_once 'CRM/Financial/BAO/PaymentProcessor.php';
460 $paymentProcessor = CRM_Financial_BAO_PaymentProcessor
::getPayment($paymentProcessorID,
464 $ipn = &self
::singleton($mode, $module, $paymentProcessor);
466 // Create new response object
467 $merchant_id = $paymentProcessor['user_name'];
468 $merchant_key = $paymentProcessor['password'];
469 $server_type = ($mode == 'test') ?
"sandbox" : '';
471 $response = new GoogleResponse($merchant_id, $merchant_key,
472 $xml_response, $server_type
474 if (GOOGLE_DEBUG_PP
) {
475 CRM_Core_Error
::debug_var('RESPONSE-ROOT', $response->root
, TRUE, TRUE, 'Google');
478 //Check status and take appropriate action
479 $status = $response->HttpAuthentication($headers);
482 case "request-received":
485 case "checkout-redirect":
486 case "merchant-calculation-callback":
489 case "new-order-notification": {
490 $response->SendAck();
491 $ipn->newOrderNotify($data[$root], $privateData, $module);
494 case "order-state-change-notification": {
495 $response->SendAck();
496 $new_financial_state = $data[$root]['new-financial-order-state']['VALUE'];
497 $new_fulfillment_order = $data[$root]['new-fulfillment-order-state']['VALUE'];
499 switch ($new_financial_state) {
501 $amount = $ipn->getAmount($orderNo);
503 $response->SendChargeOrder($data[$root]['google-order-number']['VALUE'],
504 $amount, $message_log
506 $response->SendProcessOrder($data[$root]['google-order-number']['VALUE'],
513 case 'PAYMENT_DECLINED':
515 $ipn->orderStateChange($new_financial_state, $data[$root], $module);
520 case 'CANCELLED_BY_GOOGLE':
527 case "charge-amount-notification":
528 case "chargeback-amount-notification":
529 case "refund-amount-notification":
530 case "risk-information-notification":
531 $response->SendAck();
545 function getInput(&$input, &$ids) {
546 if (!$this->getBillingID($ids)) {
550 $billingID = $ids['billing'];
551 $lookup = array("first_name" => 'contact-name',
552 // "last-name" not available with google (every thing in contact-name)
553 "last_name" => 'last_name',
554 "street_address-{$billingID}" => 'address1',
555 "city-{$billingID}" => 'city',
556 "state-{$billingID}" => 'region',
557 "postal_code-{$billingID}" => 'postal-code',
558 "country-{$billingID}" => 'country-code',
561 foreach ($lookup as $name => $googleName) {
562 $value = $dataRoot['buyer-billing-address'][$googleName]['VALUE'];
563 $input[$name] = $value ?
$value : NULL;
569 * Converts the comma separated name-value pairs in <merchant-private-data>
570 * to an array of name-value pairs.
573 function stringToArray($str) {
574 $vars = $labels = array();
575 $labels = explode(',', $str);
576 foreach ($labels as $label) {
577 $terms = explode('=', $label);
578 $vars[$terms[0]] = $terms[1];