_request = $req; if ( ! empty($this->_request['source']) ) { $this->_item_name = $this->_request['source']; } if ( ! empty($this->_request['cancel_return']) ) { $this->_cancel_return = $this->_request['cancel_return']; } if ( ! empty($this->_request['return']) ) { $this->_return = $this->_request['return']; } if ( self::OC_DEBUG ) { include_once 'CRM/Core/Error.php'; } } /** * This function accepts various arguments, processes them appropriately * and then ships the user off to the payment processor. */ function oc_donate() { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_donate()"); } // If nojavascript is a query param then we'll need to manually assign // the amount variable and also handle recurring donations. if ( $this->_request['nojavascript'] == "true" ) { if ( $this->_request['amount'] == "choose" ) { $this->_request['amount'] = $this->_request['choose_amount']; } if ( $this->_request['split'] ) { $this->_request['recur'] = $this->_request['split']; $this->_request['amount'] = round($this->_request['amount']/12, 2); } } // We don't do anything if the amount is less than the minimum amount. if ( floor($this->_request['amount']) < $this->_min_amount ) { $err_msg = "The minimum donation is \${$this->_min_amount}."; $err_msg = urlencode($err_msg); header("Location: $this->_cancel_return?{$_SERVER['QUERY_STRING']}&error=$err_msg"); exit; } if ( $this->_request['pp'] == "paypal") { $paypal_query = $this->oc_make_paypal_url(); $paypal_url = "$this->_paypal_base_url?$paypal_query"; header("Location: $paypal_url"); exit; } elseif ( $this->_request['pp'] == "gc" ) { // Fetch the credential for GC, else fail $payment_processor = $this->oc_get_paymentprocessor($this->_gc_pp_name, 'Google_Checkout'); if ( ! $payment_processor ) { $err_msg = "Failed to determine Google Checkout credentials."; $err_msg = urlencode($err_msg); header("Location: $this->_cancel_return?{$_SERVER['QUERY_STRING']}&error=$err_msg"); exit; } // Retrieve the XML for this request $gc_xml = $this->oc_make_gc_xml(); // Actually make the request $gc_url = $this->oc_gc_request($gc_xml, $payment_processor); header("Location: $gc_url"); exit; } else { $err_msg = "Unknown payment processor."; $err_msg = urlencode($err_msg); header("Location: $this->_cancel_return?{$_SERVER['QUERY_STRING']}&error=$err_msg"); exit; } } /** * This is a custom IPN/notification handler for the OneClick system. */ function oc_ipn($pp) { // Get request headers and raw POST data $headers = apache_request_headers(); $raw_post_data = file_get_contents("php://input"); if ( self::OC_DEBUG ) { $datetime = date('c'); $debug_output = "OneClick: Entering oc_ipn()\n"; $debug_output = "IPN START $datetime\nHEADERS:\n"; $debug_output .= print_r($headers, TRUE); $debug_output .= "POST DATA:\n$raw_post_data\n"; CRM_Core_Error::debug_log_message($debug_output); } if ( $pp == 'google' ) { require_once 'Google/library/googleresponse.php'; require_once 'Google/library/xml-processing/gc_xmlparser.php'; $payment_processor = $this->oc_get_paymentprocessor($this->_gc_pp_name, 'Google_Checkout'); $server_type = preg_match('/sandbox/', $this->_gc_base_url) ? 'sandbox' : ''; $response = new GoogleResponse($payment_processor['user_name'], $payment_processor['password'], $raw_post_data, $server_type); // Do the notification's credentials match those in our DB? if ( ! $status = $response->HttpAuthentication($headers) ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Google Checkout IPN unauthorized."); } header("HTTP/1.1 401 Unauthorized"); exit; } // Retrieve the root element name and data from the notification XML $xmlParser = new gc_XmlParser($raw_post_data); // The root element of the POSTed XML $root = $xmlParser->GetRoot(); $data = $xmlParser->GetData(); if ( self::OC_DEBUG ) { $debug_output = "PAYMENT PROCESSOR: Google Checkout\n"; $debug_output .= "ROOT: $root\n"; $debug_output .= "DATA:\n" . print_r($data, true); CRM_Core_Error::debug_log_message($debug_output); } // Check to see if this contribution already exists in the db, and // if it does then this is just another notification for the same // order, so we need to update the existing contribution rather // than create a new one. require_once 'CRM/Contribute/BAO/Contribution.php'; $find_params = array('trxn_id' => $data[$root]['google-order-number']['VALUE']); $existing_contr = CRM_Contribute_BAO_Contribution::retrieve( $find_params, CRM_Core_DAO::$_nullArray, CRM_Core_DAO::$_nullArray); switch ( $root ) { case 'new-order-notification': $params = $this->oc_map_ipn_fields('google', $data[$root], $existing_contr); $contribution = $this->oc_record_contribution($params); break; case 'order-state-change-notification': switch ( $data[$root]['new-financial-order-state']['VALUE'] ) { case 'PAYMENT_DECLINED': $status = 4; // Failed break; case 'CANCELLED': case 'CANCELLED_BY_GOOGLE': $status = 3; // Cancelled break; default: $status = 0; } // The payment was cancelled or declined. if ( $status ) { $params = array( 'transaction' => array( 'id' => $existing_contr->id, 'contact_id' => $existing_contr->contact_id, 'contribution_status_id' => $status ) ); $contribution = $this->oc_record_contribution($params); $notify_callbacks = TRUE; } else { // Trigger an ACK to Google $contribution = TRUE; } break; case 'risk-information-notification': // We don't need anything from this notification, so trigger an ACK // to Google. $contribution = TRUE; break; case 'cancelled-subscription-notification': $params = array( 'transaction' => array( 'id' => $existing_contr->id, 'contact_id' => $existing_contr->contact_id, 'contribution_recur_id' => $existing_contr->contribution_recur_id, 'is_recur' => '1' ), 'recur' => array( 'contribution_status_id' => '3', 'cancel_date' => $this->oc_iso8601_to_mysql($data[$root]['timestamp']['VALUE']), ) ); $contribution = $this->oc_record_contribution($params); $notify_callbacks = TRUE; break; case 'charge-amount-notification': $params = array( 'transaction' => array( 'id' => $existing_contr->id, 'contact_id' => $existing_contr->contact_id, 'contribution_status_id' => 1 ) ); $contribution = $this->oc_record_contribution($params); if ( $contribution ) { $notify_callbacks = TRUE; } break; case 'chargeback-amount-notification': // CiviCRM has no handling for the concept of a chargeback, // so we just mark the contribution as cancelled. case 'refund-amount-notification': // CiviCRM has no "refunded" contribution status, so for // these we just use 3, which is "cancelled". $params = array( 'transaction' => array( 'id' => $existing_contr->id, 'contact_id' => $existing_contr->contact_id, 'contribution_status_id' => 3 ) ); $contribution = $this->oc_record_contribution($params); if ( $contribution ) { $notify_callbacks = TRUE; } break; } // If we couldn't process the contribution for some reason then exit here // and don't ACK Google so that Google will keep trying to notify us. if ( $contribution ) { $ack = $response->SendAck($data[$root]['serial-number']); if ( self::OC_DEBUG ) { $debug_output = "OneClick:\nFORMATTED DATA: \n" . print_r($params, TRUE); $debug_output .= "CONTRIBUTION: \n" . print_r($contribution, TRUE); $debug_output .= "GOOGLE ACK: $ack\n"; CRM_Core_Error::debug_log_message($debug_output); } } else { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Google Checkout - failed to add contribution."); } header("HTTP/1.1 503 Service Unavailable"); exit; } } elseif ( $pp == 'paypal' ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: PAYMENT PROCESSOR - PayPal"); } // Verify this IPN with PayPal $verification = 'cmd=_notify-validate&' . $raw_post_data; $ch = curl_init(); curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $verification); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ch, CURLOPT_URL, $this->_paypal_base_url); $response = curl_exec($ch); $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: PayPal IPN Verification: $response"); } // If verification checks out, try to process the IPN if ( $response == 'VERIFIED' && $response_code == '200' ) { //if ( $response != 'VERIFIED' && $response_code == '200' ) { // If this is a subscr IPN, then go ahead and look up any // existing contributions that may be a part of this subscr // because we'll need this data to determine a few things like // whether to record a new contribution or to update an // existing one, or whether to record a premium for this // contribution. We don't need to worry about this for regular // web_accept payments i.e. non-recurring. if ( preg_match('/subscr_/', $this->_request['txn_type']) ) { $subscr_query = " SELECT c.id, c.contact_id, c.trxn_id, c.invoice_id, c.contribution_recur_id FROM civicrm_contribution c LEFT JOIN civicrm_contribution_recur r ON c.contribution_recur_id = r.id WHERE r.processor_id = '{$this->_request['subscr_id']}' ORDER BY c.id DESC "; $subscr_dao = CRM_Core_DAO::executeQuery( $subscr_query ); $subscr_dao->fetch(); } switch ( $this->_request['txn_type'] ) { case 'subscr_signup': case 'subscr_payment': case 'web_accept': $existing_contr = isset($subscr_dao) ? $subscr_dao : NULL; $params = $this->oc_map_ipn_fields('paypal', $this->_request, $existing_contr); $contribution = $this->oc_record_contribution($params); if ( $contribution && $contribution->contribution_status_id == '1' ) { // subscr_signup can create a contribution record, but // since it comes in at the same time as the subscr_payment // and isn't really a *true* payment notice, then only send // notifications for subscr_payment and web_accept. if ( ! $this->_request['txn_type'] == 'subscr_signup' ) { $notify_callbacks = TRUE; } } if ( self::OC_DEBUG ) { $debug_output = "OneClick: FORMATTED DATA: \n" . print_r($params, TRUE); $debug_output .= "CONTRIBUTION: \n" . print_r($contribution, TRUE); CRM_Core_Error::debug_log_message($debug_output); } break; case 'subscr_eot': $recur_update = array( 'contribution_status_id' => 1, 'end_date' => date('YmdHis'), ); break; case 'subscr_cancel': $recur_update = array( 'contribution_status_id' => 3, 'cancel_date' => date('YmdHis'), ); break; case 'subscr_failed': // This IPN type comes in when a subscr payments fails // for any given reason. PayPal will try to make the // payment two more times, and after the 3rd failure // will give up and send a subscr_eot. Even though // CiviCRM doesn't expose it, just set that status to // failed in the recur table. $recur_update = array( 'contribution_status_id' => 4, 'modified_date' => date('YmdHis'), ); } if ( $recur_update ) { $recur_update['id'] = $subscr_dao->contribution_recur_id; // Even though $subscr_dao could represent more than one // subscr payment we don't really care because // $recur_update will only exist for IPN that have to do // with the subscription, and not any one payment, so we // just set $transaction['id'] to that of the first record, // as nothing will be updated anyway, and if we don't then // oc_record_contribution will try to create a new // contribution record which we don't want. $params = array( 'transaction' => array( 'id' => $subscr_dao->id, 'contact_id' => $subscr_dao->contact_id, 'contribution_recur_id' => $subscr_dao->contribution_recur_id, 'is_recur' => '1' ), 'recur' => $recur_update ); $contribution = $this->oc_record_contribution($params); } } else { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: PayPal IPN unverified."); } exit; } } else { // We don't know what payment processor this is. return FALSE; } // Notify any URLs configured to receive notifications. A notification will go // out when a contribution is successfully recorded, and one will also go out // if the the payment processor later denies the charge or if the user cancels // the order. if ( $notify_callbacks ) { $this->oc_notify_urls($contribution); } if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: IPN END"); } } /** * Sends a contributor a custom receipt. */ function oc_send_receipt ( $contribution, $receipt_id ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_send_receipt()"); } require_once 'CRM/Core/BAO/MessageTemplates.php'; $msg_params = array('id' => $receipt_id); $template = CRM_Core_BAO_MessageTemplates::retrieve($msg_params, CRM_Core_DAO::$_nullArray); $query = "SELECT label FROM civicrm_option_value WHERE description LIKE '%OneClick Email Receipts%'"; $mail_from = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray ); if ( ! $mail_from ) { return FALSE; } $email_params = array(); $email_params['from'] = $mail_from; $email_params['toName'] = "$contribution->first_name $contribution->last_name"; $email_params['bcc'] = $mail_from; $email_params['replyTo'] = $mail_from; $email_params['subject'] = $template->msg_subject; $email_params['text'] = $template->msg_text; // get the billing location type $location_types =& CRM_Core_PseudoConstant::locationType( ); $billing_location_type_id = array_search( 'Billing', $location_types ); require_once 'CRM/Contact/BAO/Contact/Location.php'; list( $name, $email_params['toEmail'] ) = CRM_Contact_BAO_Contact_Location::getEmailDetails( $contribution->contact_id, FALSE, $billing_location_type_id ); // Make a pretty date $unix_timestamp = strtotime($contribution->receive_date); $contribution_date = date('l, F j, Y g:iA', $unix_timestamp); // Poor man's templating with preg_replace $msg_tokens = array( '%{amount}' => $contribution->total_amount, '%{date}' => $contribution_date, '%{trxn_id}' => $contribution->trxn_id, '%{first_name}' => $contribution->first_name, '%{last_name}' => $contribution->last_name ); foreach ( $msg_tokens as $msg_token => $token_value ) { $email_params['text'] = preg_replace("/$msg_token/", $token_value, $email_params['text']); } if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Sending receipt:\n{$email_params['text']}"); } require_once 'CRM/Utils/Mail.php'; CRM_Utils_Mail::send( $email_params ); // Now update receipt_date in the contribution $rcpt_params = array( 'receipt_date' => date('YmdHis') ); $rcpt_ids = array('contribution' => $contribution->id); require_once 'CRM/Contribute/BAO/Contribution.php'; CRM_Contribute_BAO_Contribution::create( $rcpt_params, $rcpt_ids ); } /** * Adds a premium to a contribution. */ function oc_set_premium( $contribution, $custom_data ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_premium()"); } // If the user wanted gifts then we need to create a new record // in the civicrm_contribution_product table. Code from here: // CRM/Contribute/Form/Contribution/Confirm.php:583-603 $gift_params = array( 'product_id' => $custom_data['premium'], 'contribution_id' => $contribution->id, 'product_option' => $custom_data['shirt_size'], 'quantity' => 1 ); //Fixed For CRM-3901 require_once 'CRM/Contribute/DAO/ContributionProduct.php'; $dao_contr_prod = & new CRM_Contribute_DAO_ContributionProduct(); $dao_contr_prod->contribution_id = $contribution->id; if ( $dao_contr_prod->find(true) ) { $gift_params['id'] = $dao_contr_prod->id; } require_once 'CRM/Contribute/BAO/Contribution.php'; CRM_Contribute_BAO_Contribution::addPremium( $gift_params ); } /** * Creates a soft contribution if the contribution originated on a PCP. */ function oc_add_soft_contribution ( $contribution, $custom_data ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_add_soft_contribution()"); } $cs_params = array(); $cs_params['contribution_id'] = $contribution->id; $cs_params['contact_id'] = $contribution->contact_id; $cs_params['amount'] = $contribution->total_amount; $cs_params['pcp_id'] = $custom_data['pcpid']; // Add user to Honor Roll unless they chose to opt-out. if ( ! isset($custom_data['sloptout']) ) { $cs_params['pcp_display_in_roll'] = '1'; $cs_params['pcp_roll_nickname'] = "$contribution->first_name $contribution->last_name"; } require_once 'CRM/Contribute/BAO/Contribution.php'; CRM_Contribute_BAO_Contribution::addSoftContribution($cs_params); } /** * Adds user to any specified groups. */ function oc_set_groups ( $contribution, $custom_data ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_groups()"); } require_once 'CRM/Contact/BAO/Group.php'; require_once 'CRM/Contact/BAO/GroupContact.php'; foreach ( $custom_data['groups'] as $group ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: User to be added to group '$group'"); } // CiviCRM replaces any spaces in group names with an underscore $group_name = preg_replace('/ /', '_', $group); $ml_params = array( "name" => $group_name, "is_active" => "1" ); $group_values = array(); CRM_Contact_BAO_Group::retrieve( $ml_params, $group_values ); $contact_ids = array($contribution->contact_id); $group_id = $group_values['id']; CRM_Contact_BAO_GroupContact::addContactsToGroup( $contact_ids, $group_id ); } } /** * Sets a db custom value so contributor will show up on a public supporter list. * WARNING: This function is CC-specific, but generally useful. */ function oc_set_supporter_list ( $contribution ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_supporter_list()"); } $query = "SELECT id FROM civicrm_custom_field WHERE label = 'OneClick Supporter List'"; $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray ); if ( ! $custom_id ) { return FALSE; } require_once 'CRM/Core/BAO/CustomValueTable.php'; $sl_params = array( 'entityID' => $contribution->id, "custom_{$custom_id}" => "Yes" ); CRM_Core_BAO_CustomValueTable::setValues($sl_params); } /** * Sets a contributor source, so we can more easily track mailing donation totals, etc */ function oc_set_contributor_source ( $contribution, $custom_data ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_set_contributor_source()"); } $query = "SELECT id FROM civicrm_custom_field WHERE label = 'Contributor Source'"; $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray ); if ( ! $custom_id ) { return FALSE; } require_once 'CRM/Core/BAO/CustomValueTable.php'; $sl_params = array( 'entityID' => $contribution->id, "custom_$custom_id" => $custom_data['contrib_source'] ); CRM_Core_BAO_CustomValueTable::setValues($sl_params); } /** * Stuff custom data into serialized array. */ function oc_create_custom_data($additional_data = NULL) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_create_custom_data()"); } $custom_data = array(); # Is this a contribution through a Personal Campaign Page? if ( isset($this->_request['pcpid']) && is_numeric($this->_request['pcpid']) ) { $custom_data['pcpid'] = trim($this->_request['pcpid']); } # Does the user want to opt-out of public supporter lists? This includes # both the public supporter page and also the "honor roll" list for PCPs. if ( isset($this->_request['sloptout']) ) { $custom_data['sloptout'] = '1'; } // Does the user want to join some mail lists? if ( isset($this->_request['groups']) ) { $groups = split(':', urldecode($this->_request['groups'])); // Make sure they are permitted groups for ( $i = 0; $i < count($groups); $i++ ) { if ( ! in_array($groups[$i], $this->_valid_groups) ) { unset($groups[$i]); } $groups[$i] = trim($groups[$i]); } $custom_data['groups'] = $groups; } // The contributor wants gifts if ( isset($this->_request['premium']) && is_numeric($this->_request['premium']) ) { $custom_data['shirt_size'] = $this->_request['size']; // This is the db ID of the premium $custom_data['premium'] = $this->_request['premium']; } // Send the contributor a receipt if ( isset($this->_request['receipt']) && is_numeric($this->_request['receipt']) ) { $custom_data['receipt'] = $this->_request['receipt']; } // If this is a recurring contribution, the ID of a final/reminder receipt if ( isset($this->_request['final_receipt']) && is_numeric($this->_request['final_receipt']) ) { $custom_data['final_receipt'] = $this->_request['final_receipt']; } // Is there a contributor source defined? if ( isset($this->_request['contrib_source']) ) { $custom_data['contrib_source'] = htmlspecialchars($this->_request['contrib_source']); } // Generally all custom data is gleaned from request variables, but // this function also accepts an argument with some additional data, // when necessary. The argument must be an array. if ( $additional_data && is_array($additional_data) ) { foreach ( $additional_data as $key => $data ) { $custom_data[$key] = $data; } } // Return serialized custom data if ( $custom_data ) { return serialize($custom_data); } else { return FALSE; } } /** * Returns a query string suitable for PayPal. */ function oc_make_paypal_url() { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_make_paypal_url()"); } // See this URL for more information on these fields // https://www.paypal.com/IntegrationCenter/ic_std-variable-ref-buy-now.html // Preset common/default PayPal variables: $paypal_fields = array( "business" => $this->_paypal_business, "cancel_return" => $this->_cancel_return, "notify_url" => $this->_paypal_notify_url, "cmd" => "_xclick", "currency_code" => "USD", "invoice" => "", "item_name" => $this->_item_name, "no_note" => "1", "no_shipping" => "1", "quantity" => "1", "return" => $this->_return, "invoice" => $this->oc_create_invoice_id(), "rm" => "2" ); // Is this a recurring contribution of some sort? // https://www.paypal.com/en_US/ebook/subscriptions/html.html if ( isset($this->_request['recur']) ) { switch ( $this->_request['recur'] ) { case '1': // Payments will recur for 12 months $paypal_fields['srt'] = "12"; case '2': $paypal_fields['cmd'] = "_xclick-subscriptions"; $paypal_fields['a3'] = round($this->_request['amount'], 2); $paypal_fields['p3'] = "1"; $paypal_fields['t3'] = "M"; $paypal_fields['src'] = "1"; $paypal_fields['sra'] = "1"; break; } } else { // Field "amount" is used in non-recurring payments $paypal_fields['amount'] = round($this->_request['amount'], 2); } // If the contributor wanted a premium then we pass this info on to PayPal. // And we also tell PayPal that we need a shipping address. // pretty and nice. if ( isset($this->_request['premium']) && is_numeric($this->_request['premium']) ) { $paypal_fields['on0'] = "Gift"; $paypal_fields['os0'] = "Yes"; $paypal_fields['on1'] = "Shirt size"; $paypal_fields['os1'] = $this->_request['size']; $paypal_fields['no_shipping'] = "2"; } else { $paypal_fields['on0'] = "Gift"; $paypal_fields['os0'] = "No"; } // Create a custom data field to be submitted to the payment processor. if ( $custom_data = $this->oc_create_custom_data() ) { $paypal_fields['custom'] = $custom_data; } // Concatenate the field with & $paypal_query_string = ''; foreach ( $paypal_fields as $key => $value ) { $value = urlencode($value); $paypal_query_string .= "$key=$value&"; } $paypal_query_string = rtrim($paypal_query_string, '&'); return $paypal_query_string; } /** * Generate XML suitable to send to Google Checkout's API. */ function oc_make_gc_xml() { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_make_gc_xml()"); } // Base XML template for Google API request if ( isset($this->_request['recur']) ) { // Template for subscription (recurring) payments $template = << 1 1 XML; } else { // Template for single-time donations $template = << 1 XML; } // A place to stick additional merchant-private-data ($custom_data). $add_custom_data = array(); $gc_xml = new DomDocument('1.0'); $gc_xml->loadXML($template); $gc_xml->encoding = 'UTF-8'; $node_item_name = $gc_xml->createTextNode($this->_item_name); $node_amount = $gc_xml->createTextNode( round($this->_request['amount'], 2) ); $gc_xml->getElementsByTagName('item-name')->item(0)->appendChild($node_item_name); $gc_xml->getElementsByTagName('unit-price')->item(0)->appendChild($node_amount); if ( isset($this->_request['recur']) ) { $node_recur_item_name = $gc_xml->createTextNode($this->_item_name); $node_recur_amount = $gc_xml->createTextNode( round($this->_request['amount'], 2) ); $node_maximum_charge = $gc_xml->createTextNode( round($this->_request['amount'], 2) ); $gc_xml->getElementsByTagName('item-name')->item(1)->appendChild($node_recur_item_name); $gc_xml->getElementsByTagName('unit-price')->item(1)->appendChild($node_recur_amount); $gc_xml->getElementsByTagName('maximum-charge')->item(0)->appendChild($node_maximum_charge); // Is this subscription just for 12 months? if ( $this->_request['recur'] == '1' ) { $attribute = $gc_xml->createAttribute('times'); $text_node = $gc_xml->createTextNode('12'); $attribute->appendChild($text_node); $gc_xml->getElementsByTagName('subscription-payment')->item(0)->appendChild($attribute); } // Unfortunately, Google doesn't provide a unique ID to indentify a // payment as belonging to a certain subscription, so we generate a // (hopefully) unique ID here and add it to our $custom_data for // later reference. $add_custom_data['subscription_id'] = md5( time() . rand() ); } // Set where user goes when order is complete at GC $node_return = $gc_xml->createTextNode($this->_return); $gc_xml->getElementsByTagName('continue-shopping-url')->item(0)->appendChild($node_return); // Set where user goes if they leave GC before finishing $node_cancel = $gc_xml->createTextNode($this->_cancel_return); $gc_xml->getElementsByTagName('edit-cart-url')->item(0)->appendChild($node_cancel); // Set up custom OneClick custom_data. if ( $custom_data = $this->oc_create_custom_data($add_custom_data) ) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: custom_data = $custom_data"); } $text_node = $gc_xml->createTextNode($custom_data); $gc_xml->getElementsByTagName('merchant-private-data')->item(0)->appendChild($text_node); if ( isset($this->_request['recur']) ) { $text_node = $gc_xml->createTextNode($custom_data); $gc_xml->getElementsByTagName('recurrent-custom-data')->item(0)->appendChild($text_node); } } $xml_request = $gc_xml->saveXML(); if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: GC XML request:\n$xml_request"); } return $xml_request; } /** * Make a CURL request to the GC API. If it's successful return the redirect * URL, else echo the error. */ function oc_gc_request($gc_xml, $payment_processor) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_gc_request()"); } $gc_url = "{$this->_gc_base_url}/{$payment_processor['user_name']}"; $gc_auth = base64_encode("{$payment_processor['user_name']}:{$payment_processor['password']}"); $header = array(); $header[] = "Authorization: Basic $gc_auth"; $header[] = "Content-type: application/xml;charset=UTF-8"; $header[] = "Accept: application/xml;charset=UTF-8"; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $gc_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $gc_xml); curl_setopt($ch, CURLOPT_HTTPHEADER, $header); // Execute the API request. $gc_response = curl_exec($ch); $response_xml = new DomDocument('1.0'); $response_xml->loadXML($gc_response); $response_type = $response_xml->documentElement->nodeName; switch ($response_type) { case "checkout-redirect": return $response_xml->getElementsByTagName('redirect-url')->item(0)->nodeValue; break; case "error": $error = $response_xml->getElementsByTagName('error-message')->item(0)->nodeValue; echo "Google Checkout returned error:\n\n$error"; exit; break; default: $data = $response_xml->saveXML(); echo "Unknown response from Google Checkout:\n\n$data"; exit; } } /** * Generate a unique CiviCRM invoice number. */ function oc_create_invoice_id() { // CRM/Contribute/Form/Contribution/Main.php:849 return md5(uniqid(rand(), TRUE)); } /** * Post contribution and contributor details to each of an * array of URLs. */ function oc_notify_urls($notification) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_notify_urls()"); } if ( ! $this->_notify_urls ) { return false; } $json_data = json_encode($notification); $hash = sha1(CIVICRM_SITE_KEY . $json_data); $post_data = "data=$json_data"; $post_data .= "&hash=$hash"; $ch = curl_init(); curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); foreach ( $this->_notify_urls as $notify_url ) { curl_setopt($ch, CURLOPT_URL, $notify_url); $response = curl_exec($ch); $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ( $status_code != '200' ) { if ( self::OC_DEBUG ) { $debug_output = "Notify URL '$notify_url' returned HTTP status code: '$status_code'\n"; $debug_output .= "The response was:\n$response"; CRM_Core_Error::debug_log_message("OneClick: $debug_output"); } } } } /** * Fetch payment processor object from CiviCRM database. */ function oc_get_paymentprocessor($pp_name, $pp_type) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_get_paymentprocessor()"); } require_once 'CRM/Core/BAO/PaymentProcessor.php'; // This means that we have to be consistent about naming our GC payment // processor in CiviCRM $params = array( 'name' => $pp_name, 'payment_processor_type' => $pp_type, 'is_active' => 1, 'is_test' => 0 ); $payment_processor = array(); CRM_Core_BAO_PaymentProcessor::retrieve($params, $payment_processor); if ( $payment_processor ) { return $payment_processor; } else { return FALSE; } } /** * Stores or fetches OneClick custom data in/from a CiviCRM custom data field. * This could be useful to record, and we can't be guaranteed that every IPN * will contain this data, but we may need it. This is somewhat of a hack, but * to stay away from coding for a database ID we'll just require that the custom * data field label is "OneClick Custom Data" so that we can find it in the DB. */ function oc_store_fetch_custom_data($operation, $contribution_id, $custom_data = NULL) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_store_fetch_custom_data()"); } if ( ! $operation || ! $contribution_id ) { return FALSE; } $return = FALSE; $query = "SELECT id FROM civicrm_custom_field WHERE label = 'OneClick Custom Data'"; $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray ); if ( $custom_id ) { require_once 'CRM/Core/BAO/CustomValueTable.php'; if ( $operation == 'setValues' ) { $custom_value = serialize($custom_data); } else { $custom_value = 1; } $custom_params = array( 'entityID' => $contribution_id, "custom_{$custom_id}" => $custom_value ); $custom_value = CRM_Core_BAO_CustomValueTable::$operation($custom_params); if ( ! $custom_value['is_error'] ) { if ( $operation == 'getValues' ) { $return = unserialize($custom_value["custom_{$custom_id}"]); } elseif ( $operation == 'setValues' ) { $return = TRUE; } } } return $return; } /** * Stores the payment processor in a custom field for the contribution. */ function oc_set_payment_processor($payment_processor, $contribution_id) { if ( ! $payment_processor || ! $contribution_id ) { return FALSE; } $return = FALSE; $query = "SELECT id FROM civicrm_custom_field WHERE label = 'Payment Processor'"; $custom_id = CRM_Core_DAO::singleValueQuery( $query, CRM_Core_DAO::$_nullArray ); if ( $custom_id ) { require_once 'CRM/Core/BAO/CustomValueTable.php'; $custom_params = array( 'entityID' => $contribution_id, "custom_{$custom_id}" => $payment_processor ); $custom_value = CRM_Core_BAO_CustomValueTable::setValues($custom_params); if ( ! $custom_value['is_error'] ) { $return = TRUE; } } return $return; } /** * Converts ISO-8601 dates to MySQL suitable dates. */ function oc_iso8601_to_mysql($date) { $unix_timestamp = strtotime($date); $date_field = date('YmdHis', $unix_timestamp); return $date_field; } /** * Takes the POSTed input from the payment processor IPN and normalizes the * data. */ function oc_map_ipn_fields($payment_processor, $ipn_data, &$existing_contr) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_map_ipn_fields()"); } $contact = array(); $email = array(); $address = array(); $recur = array(); $contribution = array(); // This value gets stored in a custom field in the contribution // making searching based on payment processor easy $contribution['payment_processor'] = $payment_processor; if ( $payment_processor == 'paypal' ) { // Contact information $contact['first_name'] = $ipn_data['first_name']; $contact['last_name'] = $ipn_data['last_name']; // Email address $email['email'] = $ipn_data['payer_email']; // Address information if ( $ipn_data['address_name'] ) { $address['address_name'] = $ipn_data['address_name']; $address['street_address'] = $ipn_data['address_street']; $address['city'] = $ipn_data['address_city']; $address['state_province'] = $ipn_data['address_state']; $address['postal_code'] = $ipn_data['address_zip']; $address['country'] = $ipn_data['address_country_code']; } // Common contribution information $contribution['source'] = $ipn_data['item_name']; if ( $ipn_data['memo'] ) { $contribution['note'] = $ipn_data['memo']; } // The fields in the IPN change depending on the transaction type. // web_accept and subscr_payment are mostly identical. switch ( $ipn_data['txn_type'] ) { case 'subscr_signup': // If N==1, then the subscr_payment IPN already arrived, so // we set these fields so that this IPN updates the // existing record rather recording a new one. if ( $existing_contr->N == 1 ) { $contribution['id'] = $existing_contr->id; $contribution['contact_id'] = $existing_contr->contact_id; $contribution['contribution_recur_id'] = $existing_contr->contribution_recur_id; } else { // subscr_signup came in first, so mark contr. as // pending and set the invoice_id temporarily (later it // will be changed to be the same as the trxn_id when // the subscr_payment IPN arrives) $contribution['contribution_status_id'] = '2'; $contribution['invoice_id'] = $ipn_data['invoice']; } $contribution['total_amount'] = $ipn_data['mc_amount3']; $contribution['currency'] = $ipn_data['mc_currency']; $recur['amount'] = $ipn_data['mc_amount3']; $recur['start_date'] = $this->oc_iso8601_to_mysql($ipn_data['subscr_date']); $recur['processor_id'] = $ipn_data['subscr_id']; // PayPal sends two distinct data elements in a single field, // so we break them out here. $freq_units = array ( 'D' => 'day', 'W' => 'week', 'M' => 'month', 'Y' => 'year' ); list($frequency_interval, $frequency_unit) = split(' ', $ipn_data['period3']); $recur['frequency_interval'] = $frequency_interval; $recur['frequency_unit'] = $freq_units[$frequency_unit]; $recur['installments'] = $ipn_data['recur_times']; $contribution['is_recur'] = TRUE; // This IPN only arrives at the time of the first payment, // and it's only at the time of the first payment that we // want to record a premium for this subscr. $contribution['record_premium'] = TRUE; break; case 'subscr_payment': // If N==1 and trxn_id is NULL then the subscr_signup IPN // came in before this IPN and this must be the // subscr_payment IPN for the very first payment, so we'll // update the existing record instead of creating a new // one. Similarly, if the txn_id of the IPN is equal to // the trxn_id of the existing contribution, then this must // be an update to an existing record, and we don't want to // create a new record. This latter case can happen when // someone pays with an eCheck, where we get two // subscr_payment IPN, one to notify that it's pending and // the next to notify that it's completed. if ( ($existing_contr->N == 1 && empty($existing_contr->trxn_id)) || ($existing_contr->trxn_id == $ipn_data['txn_id']) ) { $contribution['id'] = $existing_contr->id; $contribution['contact_id'] = $existing_contr->contact_id; $contribution['contribution_recur_id'] = $existing_contr->contribution_recur_id; } elseif ( $existing_contr->N >= 1 ) { // This must simply be a subsequent subscr payment, in // which case we want to create a new contribution // record, but reuse the recur_id and contact_id $contribution['contact_id'] = $existing_contr->contact_id; $contribution['contribution_recur_id'] = $existing_contr->contribution_recur_id; } $contribution['is_recur'] = TRUE; $recur['modified_date'] = $this->oc_iso8601_to_mysql($ipn_data['payment_date']); $recur['trxn_id'] = $ipn_data['txn_id']; $recur['invoice_id'] = $ipn_data['txn_id']; $recur['processor_id'] = $ipn_data['subscr_id']; // NOTE we do not break here, but proceed to web_accept case 'web_accept': $contribution['trxn_id'] = $ipn_data['txn_id']; $contribution['invoice_id'] = $ipn_data['txn_id']; $contribution['total_amount'] = $ipn_data['mc_gross']; $contribution['fee_amount'] = $ipn_data['mc_fee']; $contribution['net_amount'] = $ipn_data['mc_gross'] - $ipn_data['mc_fee']; $contribution['currency'] = $ipn_data['mc_currency']; $contribution['receive_date'] = $this->oc_iso8601_to_mysql($ipn_data['payment_date']); if ( $ipn_data['payment_status'] == 'Completed' ) { $contribution['contribution_status_id'] = '1'; } else { $contribution['contribution_status_id'] = '2'; // Pending } // web_accept IPN are for single time donations, and all // single donations need to have any premium recorded, but // since the subscr_payment IPN passes into this block, we // need to check for the IPN type again if ( $ipn_data['txn_type'] == 'web_accept' ) { $contribution['record_premium'] = TRUE; } } // Extract the contents of any custom data if ( $ipn_data['custom'] ) { $contribution['custom_data'] = unserialize($ipn_data['custom']); } } elseif ( $payment_processor == 'google' ) { $gc_contact = $ipn_data['buyer-shipping-address']; $gc_order = $ipn_data['shopping-cart']['items']['item']; // Extract the contents of any custom data. Google doesn't send merchant-private-data // with subsequent payments in a subscription, so we have to stick it in our own field. if ( isset($gc_order['merchant-private-item-data']['recurrent-custom-data']) ) { $contribution['custom_data'] = unserialize($gc_order['merchant-private-item-data']['recurrent-custom-data']['VALUE']); } elseif ( isset($ipn_data['shopping-cart']['merchant-private-data']) ) { $contribution['custom_data'] = unserialize($ipn_data['shopping-cart']['merchant-private-data']['VALUE']); } // If the contribution already exists in the database then we can // assume that we've already recorded all the relevant information // for the contact and can just pass on the existing contact_ id, // and we can also pass along the contribution id so that we update // the existing record rather than creating a new one. if ( $existing_contr ) { $contribution['id'] = $existing_contr->id; $contribution['contact_id'] = $existing_contr->contact_id; } else { // Contact name if ( isset($gc_contact['structured-name']) ) { $contact['first_name'] = $gc_contact['structured-name']['first-name']['VALUE']; $contact['last_name'] = $gc_contact['structured-name']['last-name']['VALUE']; } else { $contact['full_name'] = $gc_contact['contact-name']['VALUE']; } // Email address $email['email'] = $gc_contact['email']['VALUE']; // Address information $address['address_name'] = $gc_contact['contact-name']['VALUE']; $address['street_address'] = $gc_contact['address1']['VALUE']; if ( isset($gc_contact['address2']) ) { $address['supplemental_address_1'] = $gc_contact['address2']['VALUE']; } $address['city'] = $gc_contact['city']['VALUE']; $address['state_province'] = $gc_contact['region']['VALUE']; $address['postal_code'] = $gc_contact['postal-code']['VALUE']; $address['country'] = $gc_contact['country-code']['VALUE']; } // Contribution information $contribution['source'] = $gc_order['item-name']['VALUE']; $contribution['trxn_id'] = $ipn_data['google-order-number']['VALUE']; $contribution['invoice_id'] = $contribution['trxn_id']; $contribution['total_amount'] = $ipn_data['order-total']['VALUE']; $contribution['currency'] = $ipn_data['order-total']['currency']; $contribution['receive_date'] = $this->oc_iso8601_to_mysql($ipn_data['timestamp']['VALUE']); // This function is only called by new-order-notifications and we // have several more notifications before this order is done, so // set this to Pending. $contribution['contribution_status_id'] = '2'; // This is a subscription (recurring) contribution. if ( isset($gc_order['subscription']) ) { $recur['amount'] = $contribution['total_amount']; $recur['start_date'] = $this->oc_iso8601_to_mysql($contribution['receive_date']); $recur['modified_date'] = $this->oc_iso8601_to_mysql($contribution['receive_date']); $contribution['is_recur'] = 1; $freqUnits = array ( 'DAILY' => 'day', 'WEEKLY' => 'week', 'MONTHLY' => 'month', 'YEARLY' => 'year' ); $recur['frequency_unit'] = $freqUnits[$gc_order['subscription']['period']]; $recur['frequency_interval'] = 1; $recur['installments'] = $gc_order['subscription']['payments']['subscription-payment']['times']; // This is a number we generated and stuck in merchant-private-data. $recur['processor_id'] = $contribution['custom_data']['subscription_id']; $recur['trxn_id'] = $contribution['trxn_id']; $recur['invoice_id'] = $contribution['invoice_id']; // The element is only present in the first // new-order-notification of a subscription and it's only at // the time of the first payment that we want to record a // premium for this subscr. $contribution['record_premium'] = TRUE; } elseif ( isset($gc_order['merchant-private-item-data']['recurrent-custom-data']) ) { // We will enter into this block if this is any other than the first // payment in a subscription i.e. subsequent payments. $recur['trxn_id'] = $contribution['trxn_id']; $recur['invoice_id'] = $contribution['invoice_id']; $recur['modified_date'] = $this->oc_iso8601_to_mysql($contribution['receive_date']); $contribution['is_recur'] = 1; // We have to use our generated subscription_id to find out which record in the // contribution_recur table this belongs to. if ( isset($contribution['custom_data']) ) { require_once 'CRM/Contribute/BAO/ContributionRecur.php'; $existing_recur_obj = new CRM_Contribute_BAO_ContributionRecur; $existing_recur_obj->processor_id = $contribution['custom_data']['subscription_id']; if ( $existing_recur_obj->find(true) ) { $recur['id'] = $existing_recur_obj->id; } } } } // If this is not a recurring contribution, for which only the first // payment should have a premium recorded, then we record a premium in // every case if ( ! $contribution['is_recur'] ) { $contribution['record_premium'] = TRUE; } $contact['email'][1] = $email; $contact['transaction'] = $contribution; if ( ! empty($address) ) { $contact['address'][1] = $address; } if ( ! empty($recur) ) { $contact['recur'] = $recur; } return $contact; } /** * Records a contribution based on IPN data. This should be payment processor * agnostic. */ function oc_record_contribution($params) { if ( self::OC_DEBUG ) { CRM_Core_Error::debug_log_message("OneClick: Entering oc_record_contribution()"); } $transaction = $params['transaction']; if ( $params['recur'] ) { $recur = $params['recur']; } // If contact_id is already set then we can just fetch all the related // data for the contact, else we'll need to record it all new. require_once 'CRM/Contact/BAO/Contact.php'; if ( $transaction['contact_id'] ) { $find_params = array('contact_id' => $transaction['contact_id']); $contact = CRM_Contact_BAO_Contact::retrieve($find_params, CRM_Core_DAO::$_nullArray, CRM_Core_DAO::$_nullArray); } else { // Get the address and email fields configured. The following is // largely borrowed from: CRM/Contribute/BAO/Contribution/Utils.php $billing_loc_type_id = CRM_Core_DAO::getFieldValue( 'CRM_Core_DAO_LocationType', 'Billing', 'id', 'name' ); if ( ! $billing_loc_type_id ) { $billing_loc_type_id = 1; } if ( ! CRM_Utils_System::isNull($params['address']) ) { $params['address'][1]['is_primary'] = 1; $params['address'][1]['location_type_id'] = $billing_loc_type_id; } if ( ! CRM_Utils_System::isNull($params['email']) ) { $params['email'][1]['location_type_id'] = $billing_loc_type_id; } // Just assume that all donations via a payment processor are of // type Individual. $params['contact_type'] = 'Individual'; // Add contact using dedupe rule require_once 'CRM/Dedupe/Finder.php'; $dedupe_params = CRM_Dedupe_Finder::formatParams ($params, 'Individual'); $dupe_ids = CRM_Dedupe_Finder::dupesByParams($dedupe_params, 'Individual'); // if we find more than one contact, use the first one if ( CRM_Utils_Array::value( 0, $dupe_ids ) ) { $params['contact_id'] = $dupe_ids[0]; } $contact = CRM_Contact_BAO_Contact::create($params); if ( $contact->id ) { $transaction['contact_id'] = $contact->id; } else { return FALSE; } } // Set to default contribution Type (Donation) unless it was already passed. if ( ! isset($transaction['contribution_type_id']) ) { require_once 'CRM/Contribute/BAO/ContributionType.php'; $params = array('name' => 'Donation'); $contribution_type = CRM_Contribute_BAO_ContributionType::retrieve($params, CRM_Core_DAO::$_nullArray); if ( $contribution_type ) { $transaction['contribution_type_id'] = $contribution_type->id; } else { // This is what CiviCRM does as a complete fallback method. // CRM/Contribute/BAO/Contribution/Utils.php:410 require_once 'CRM/Contribute/PseudoConstant.php'; $contributionTypes = array_keys( CRM_Contribute_PseudoConstant::contributionType( ) ); $transaction['contribution_type_id'] = $contributionTypes[0]; } } // Handle a subscription payment before the contribution if ( $transaction['is_recur'] && $recur ) { require_once 'CRM/Contribute/BAO/ContributionRecur.php'; $recur_obj = new CRM_Contribute_BAO_ContributionRecur; if ( $transaction['contribution_recur_id'] ) { $recur_obj->id = $transaction['contribution_recur_id']; } $recur['contact_id'] = $contact->id; $recur_obj->copyValues($recur); $recur_obj->save(); if ( self::OC_DEBUG ) { $debug_output = "RECURRING OBJECT:\n" . print_r($recur_obj, TRUE); CRM_Core_Error::debug_log_message($debug_output); } $transaction['contribution_recur_id'] = $recur_obj->id; } $ids = array(); $ids['contribution'] = $transaction['id'] ? $transaction['id'] : ''; require_once 'CRM/Contribute/BAO/Contribution.php'; $contribution = CRM_Contribute_BAO_Contribution::create($transaction, $ids); if ( is_a( $contribution, 'CRM_Core_Error') ) { CRM_Core_Error::debug_log_message($contribution->_errors[0]['message']); return FALSE; } if ( $contribution ) { // When a contribution is added/update with create() (above), the // returned contribution object may only contain the fields that were // input to create(), but more often than not we're going to want every // data point in the contribution, so we just look it up and re-capture // it here. $find_params = array('id' => $contribution->id); $contribution = CRM_Contribute_BAO_Contribution::retrieve( $find_params, CRM_Core_DAO::$_nullArray, CRM_Core_DAO::$_nullArray); // If we end up pinging the notification callback URLs, it's nice // for us to include some contact info in the data we send. These // fields are also used by some of the oc_ functions. $contribution->first_name = $contact->first_name; $contribution->last_name = $contact->last_name; $contribution->full_name = $contact->display_name; $email = array_shift($contact->email); $contribution->email = $email->email; // Here we record which payment processor was used. $this->oc_set_payment_processor($transaction['payment_processor'], $contribution->id); // The OneClick process packs a number of data elements into an array // called custom_data. We store that data in a CiviCRM custom data // field with the contribution for future reference. We may use it for // anything, but in specific the motivating factor to store it is // Google Checkout, since Google doesn't send custom data // (merchant-private-data) with charge-amount-notifications and it is // for this notification that we need the email donation receipt ID // stored in the custom data. First Check to see if any custom data has // already been stored. $has_custom_data = $this->oc_store_fetch_custom_data('getValues', $contribution->id); if ( isset($transaction['custom_data']) ) { $custom_data = $transaction['custom_data']; if ( ! $has_custom_data ) { $this->oc_store_fetch_custom_data('setValues', $contribution->id, $custom_data); } } else { $custom_data = $has_custom_data; } if ( $custom_data ) { // Add a premium to the contribution if ( $custom_data['premium'] && $transaction['record_premium'] ) { $this->oc_set_premium( $contribution, $custom_data ); } // This is a contribution for a PCP. if ( array_key_exists("pcpid", $custom_data) ) { $this->oc_add_soft_contribution( $contribution, $custom_data ); } // The contributor wants to join some mail lists (groups) if ( array_key_exists("groups", $custom_data) && $custom_data['groups'] ) { $this->oc_set_groups( $contribution, $custom_data ); } // Add user to public supporter lists unless they opted out. if ( ! isset($custom_data['sloptout']) ) { $this->oc_set_supporter_list( $contribution ); } // Store the contributor's donation source // This allows us to more easily track total contributions from // mailings, special events, the widget, or other external source if ( array_key_exists("contrib_source", $custom_data) ) { $this->oc_set_contributor_source( $contribution, $custom_data ); } } else { // If no custom data exists, then the default is to add someone // to supporter lists. $this->oc_set_supporter_list( $contribution ); } // Send the contributor a receipt, but only if the transaction is complete // and we haven't already sent the user a receipt. if ( $contribution->contribution_status_id == '1' && ! $contribution->receipt_date ) { if ( $custom_data && $custom_data['receipt'] ) { $receipt_id = $custom_data['receipt']; // If this is a recurring contribution, then we should check to see // if this is the last payment in the subscription. If it is, then // we may send a special receipt that prompts the user to renew. if ( $contribution->contribution_recur_id ) { $query = " SELECT installments, count(contr.id) AS payments FROM civicrm_contribution_recur AS recur JOIN civicrm_contribution AS contr ON recur.id = contr.contribution_recur_id WHERE recur.id = '$contribution->contribution_recur_id' AND recur.installments IS NOT NULL AND recur.installments <> '' GROUP BY recur.id; "; $recur_dao = CRM_Core_DAO::executeQuery( $query ); $recur_dao->fetch(); if ( isset($recur_dao->installments) && isset($recur_dao->payments) ) { if ( $recur_dao->installments == $recur_dao->payments ) { if ( isset($custom_data['final_receipt'] ) ) { $receipt_id = $custom_data['final_receipt']; } } } } $this->oc_send_receipt($contribution, $receipt_id); } } } return $contribution; } } ?>