/** * Create a new recurring subscription. * * Creates a new recurring subscription and processes a charge for today. * * @param int $client_id The Client ID * @param int $gateway_id The gateway ID to process this charge with * @param float $amount The amount to charge (e.g., "50.00") * @param array $credit_card The credit card information * @param int $credit_card['card_num'] The credit card number * @param int $credit_card['exp_month'] The credit card expiration month in 2 digit format (01 - 12) * @param int $credit_card['exp_year'] The credit card expiration year (YYYY) * @param string $credit_card['name'] The credit card cardholder name. Required only is customer ID is not supplied. * @param int $credit_card['cvv'] The Card Verification Value. Optional * @param int $customer_id The ID of the customer to link the charge to * @param array $customer An array of customer data to create a new customer with, if no customer_id * @param float $customer_ip The optional IP address of the customer * @param array $recur The details for a recurring charge * @param int $recur['plan_id'] The ID of the plan to pull recurring details from (Optional) * @param string $recur['start_date'] The start date of the subscription * @param string $recur['end_date'] The end date of the subscription * @param int $recur['free_trial'] The number of days to give a free trial before. Will combine with start_date if that is also set. (Optional) * @param float $recur['amount'] The amount to charge every INTERVAL days. If not there, the main $amount will be used. * @param int $recur['occurrences'] The total number of occurrences (Optional, if end_date doesn't exist). * @param string $recur['notification_url'] The URL to send POST updates to for notices re: this subscription. * @param string $return_url The URL for external payment processors to return the user to after payment * @param string $cancel_url The URL to send if the user cancels an external payment * @param int $renew The subscription that is being renewed, if there is one * @param string $coupon A coupon code * * @return mixed Array with response_code and response_text */ function Recur($client_id, $gateway_id, $amount = FALSE, $credit_card = array(), $customer_id = FALSE, $customer = array(), $customer_ip = FALSE, $recur = array(), $return_url = FALSE, $cancel_url = FALSE, $renew = FALSE, $coupon = FALSE) { $CI =& get_instance(); $CI->load->library('field_validation'); // Clean up our url's if ($return_url) { $return_url = htmlspecialchars_decode($return_url); } if ($cancel_url) { $cancel_url = htmlspecialchars_decode($cancel_url); } // Get the gateway info to load the proper library $gateway = $this->GetGatewayDetails($client_id, $gateway_id); if (!$gateway or $gateway['enabled'] == '0') { die($this->response->Error(5017)); } // load the gateway $gateway_name = $gateway['name']; $CI->load->library('payment/' . $gateway_name); $gateway_settings = $this->{$gateway_name}->Settings(); $this->load->library('field_validation'); // credit card validation has been moved after the 2nd coupon check, in case this coupon makes it free // are we linking this to another sub via renewal? if (!empty($renew)) { $CI->load->model('recurring_model'); $renewed_subscription = $CI->recurring_model->GetSubscriptionDetails($client_id, $renew); if (!empty($renewed_subscription)) { $mark_as_renewed = $renewed_subscription['subscription_id']; /** * automatically set start date * we don't do this because Membrr does it automatically, and want to give * the developer more control. * if (strtotime($renewed_subscription['next_charge_date']) > time()) { * $recur['start_date'] = $renewed_subscription['next_charge_date']; * } * else { * $recur['start_date'] = date('Y-m-d'); * } * * $recur['free_trial'] = 0; */ } else { $mark_as_renewed = FALSE; } } else { $mark_as_renewed = FALSE; } // Get the customer details if a customer id was included $this->load->model('customer_model'); if (!empty($customer_id)) { $customer = $CI->customer_model->GetCustomer($client_id, $customer_id); $customer['customer_id'] = $customer['id']; $created_customer = FALSE; } elseif (isset($customer) and !empty($customer)) { // look for embedded customer information // by Getting the customer after it's creation, we get a nice clean ISO2 code for the country $customer_id = $CI->customer_model->NewCustomer($client_id, $customer); $customer = $CI->customer_model->GetCustomer($client_id, $customer_id); $customer['customer_id'] = $customer_id; unset($customer_id); $created_customer = TRUE; } else { // no customer_id or customer information - is this a problem? // we'll check if this gateway required customer information if ($gateway_settings['requires_customer_information'] == 1) { die($this->response->Error(5018)); } // no customer information was passed but this gateway is OK with that, let's just get the customer first/last name // from the credit card name for our records if (!isset($credit_card['name'])) { die($this->response->Error(5004)); } else { $name = explode(' ', $credit_card['name']); $customer['first_name'] = $name[0]; $customer['last_name'] = $name[count($name) - 1]; $customer['customer_id'] = $CI->customer_model->SaveNewCustomer($client_id, $customer['first_name'], $customer['last_name']); $created_customer = TRUE; } } // if we have an IP, we'll populate this field // note, if we get an error later: the first thing we check is to see if an IP is required // by checking this *after* an error, we give the gateway a chance to be flexible and, if not, // we give the end-user the most likely error response if (!empty($customer_ip)) { // place it in $customer array $customer['ip_address'] = $customer_ip; } else { $customer['ip_address'] = ''; } if (isset($recur['plan_id'])) { // we have a linked plan, let's load that information $CI->load->model('plan_model'); $plan_details = $CI->plan_model->GetPlanDetails($client_id, $recur['plan_id']); $interval = isset($recur['interval']) ? $recur['interval'] : $plan_details->interval; $notification_url = isset($recur['notification_url']) ? $recur['notification_url'] : $plan_details->notification_url; $free_trial = isset($recur['free_trial']) ? $recur['free_trial'] : $plan_details->free_trial; $occurrences = isset($recur['occurrences']) ? $recur['occurrences'] : $plan_details->occurrences; // calculate first charge amount: // 1) First charge is main $amount if given // 2) If no $recur['amount'], use plan amount // 3) Else use $recur['amount'] if (isset($amount)) { $amount = $amount; } elseif (isset($recur['amount'])) { $amount = $recur['amount']; } elseif (isset($plan_details->amount)) { $amount = $plan_details->amount; } $amount = $this->field_validation->ValidateAmount($amount); if ($amount === FALSE) { die($this->response->Error(5009)); } // store plan ID $plan_id = $plan_details->plan_id; } else { if (!isset($recur['interval']) or !is_numeric($recur['interval'])) { die($this->response->Error(5011)); } else { $interval = $recur['interval']; } // Check for a notification URL $notification_url = isset($recur['notification_url']) ? $recur['notification_url'] : ''; // Validate the amount if ($this->field_validation->ValidateAmount($amount) === FALSE) { die($this->response->Error(5009)); } $plan_id = 0; $free_trial = (isset($recur['free_trial']) and is_numeric($recur['free_trial'])) ? $recur['free_trial'] : FALSE; } // Validate the start date to make sure it is in the future if (isset($recur['start_date'])) { // adjust to server time $recur['start_date'] = server_time($recur['start_date'], 'Y-m-d', true); if (!$this->field_validation->ValidateDate($recur['start_date']) or $recur['start_date'] < date('Y-m-d')) { die($this->response->Error(5001)); } else { $start_date = date('Y-m-d', strtotime($recur['start_date'])); } } else { $start_date = date('Y-m-d'); } // coupon check if (!empty($coupon)) { $CI->load->model('coupon_model'); $coupon = $CI->coupon_model->get_coupons($client_id, array('coupon_code' => $coupon)); if (!empty($coupon) and $CI->coupon_model->is_eligible($coupon, isset($recur['plan_id']) ? $recur['plan_id'] : FALSE, isset($customer_id) ? $customer_id : FALSE)) { $coupon = $coupon[0]; $free_trial = $CI->coupon_model->subscription_adjust_trial($free_trial, $coupon['type_id'], $coupon['trial_length']); $coupon_id = $coupon['id']; } else { $coupon_id = FALSE; } } else { $coupon_id = FALSE; } // do we have to adjust the start_date for a free trial? if ($free_trial) { $start_date = date('Y-m-d', strtotime($start_date) + $free_trial * 86400); } // get the next payment date if (date('Y-m-d', strtotime($start_date)) == date('Y-m-d')) { // deal with the same_day_every_month mod for some gateways if (isset($this->{$gateway_name}->same_day_every_month) and $this->{$gateway_name}->same_day_every_month == TRUE and $interval % 30 === 0) { $months = $interval / 30; $plural = $months > 1 ? 's' : ''; $next_charge_date = date('Y-m-d', strtotime('today + ' . $months . ' month' . $plural)); } else { $next_charge_date = date('Y-m-d', strtotime($start_date) + $interval * 86400); } } else { $next_charge_date = date('Y-m-d', strtotime($start_date)); } // if an end date was passed, make sure it's valid if (isset($recur['end_date'])) { // adjust to server time $recur['end_date'] = !isset($end_date_set_by_server) ? server_time($recur['end_date']) : $recur['end_date']; if (strtotime($recur['end_date']) < time()) { // end_date is in the past die($this->response->Error(5002)); } elseif (strtotime($recur['end_date']) < strtotime($start_date)) { // end_date is before start_date die($this->response->Error(5003)); } else { // date is good $end_date = date('Y-m-d', strtotime($recur['end_date'])); } } elseif (isset($occurrences) and !empty($occurrences)) { // calculate end_date from # of occurrences as defined by plan $end_date = date('Y-m-d', strtotime($start_date) + $interval * 86400 * $occurrences); } elseif (isset($recur['occurrences']) and !empty($recur['occurrences'])) { // calculate end_date from # of occurrences from recur node $end_date = date('Y-m-d', strtotime($start_date) + $interval * 86400 * $recur['occurrences']); } else { // calculate the end_date based on the max end date setting $end_date = date('Y-m-d', strtotime($start_date) + $this->config->item('max_recurring_days_from_today') * 86400); } if (!empty($credit_card) and isset($credit_card['exp_year']) and !empty($credit_card['exp_year'])) { // if the credit card expiration date is before the end date, we need to set the end date to one day before the expiration $check_year = $credit_card['exp_year'] > 2000 ? $credit_card['exp_year'] : '20' . $credit_card['exp_year']; $expiry = mktime(0, 0, 0, $credit_card['exp_month'], days_in_month($credit_card['exp_month'], $credit_card['exp_year']), $check_year); $date = strtotime($next_charge_date); while ($date < strtotime($end_date)) { if ($expiry < $date) { $end_date = date('Y-m-d', $date); break; } $date = $date + $interval * 86400; } } // adjust end date if it's less than next charge if (strtotime($end_date) <= strtotime($next_charge_date)) { // set end date to next charge $end_date = $next_charge_date; $total_occurrences = 1; } // figure the total number of occurrences $total_occurrences = round((strtotime($end_date) - strtotime($start_date)) / ($interval * 86400), 0); if ($total_occurrences < 1) { // the CC expiry date is only going to allow 1 charge $total_occurrences = 1; } // if they sent an $amount with their charge, this means that their first charge is different // so now we need to grab the true recurring amount if (isset($recur['amount'])) { $recur['amount'] = $recur['amount']; } elseif (isset($plan_details) and is_object($plan_details) and isset($plan_details->amount)) { $recur['amount'] = $plan_details->amount; } else { $recur['amount'] = $amount; } // 2nd coupon check - adjust amounts if (!empty($coupon_id)) { $CI->coupon_model->subscription_adjust_amount($amount, $recur['amount'], $coupon['type_id'], $coupon['reduction_type'], $coupon['reduction_amt']); } // validate function arguments if (empty($credit_card) and ((double) $recur['amount'] > 0 or (double) $amount > 0) and $gateway_settings['external'] == FALSE and $gateway_settings['no_credit_card'] == FALSE) { die($CI->response->Error(1004)); } if (!empty($credit_card)) { // Validate the Credit Card number $credit_card['card_num'] = trim(str_replace(array(' ', '-'), '', $credit_card['card_num'])); $credit_card['card_type'] = $this->field_validation->ValidateCreditCard($credit_card['card_num'], $gateway); if (!$credit_card['card_type']) { die($this->response->Error(5008)); } if (!isset($credit_card['exp_month']) or empty($credit_card['exp_month'])) { die($this->response->Error(5008)); } if (!isset($credit_card['exp_year']) or empty($credit_card['exp_year'])) { die($this->response->Error(5008)); } } // Save the subscription info $CI->load->model('recurring_model'); $card_last_four = isset($credit_card['card_num']) ? substr($credit_card['card_num'], -4, 4) : '0'; $subscription_id = $CI->recurring_model->SaveRecurring($client_id, $gateway['gateway_id'], $customer['customer_id'], $interval, $start_date, $end_date, $next_charge_date, $total_occurrences, $notification_url, $recur['amount'], $plan_id, $card_last_four, $coupon_id); // get subscription $subscription = $CI->recurring_model->GetRecurring($client_id, $subscription_id); // is there a charge for today? $charge_today = ((double) $amount > 0 and date('Y-m-d', strtotime($subscription['date_created'])) == date('Y-m-d', strtotime($subscription['start_date']))) ? TRUE : FALSE; // set last_charge as today, if today was a charge if ($charge_today === TRUE) { $CI->recurring_model->SetChargeDates($subscription_id, date('Y-m-d', strtotime($subscription['date_created'])), $next_charge_date); } // if amount is greater than 0, we require a gateway to process if ($recur['amount'] > 0) { // recurring charges are not free $response = $CI->{$gateway_name}->Recur($client_id, $gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences, $return_url, $cancel_url); } elseif ($recur['amount'] <= 0 and $amount > 0) { // recurring charges are free, but there is an initial charge // can't be an external gateway if ($gateway_settings['external'] == TRUE) { die($this->response->Error(5024)); } // must have a start date of today if ($charge_today !== TRUE) { die($this->response->Error(5025)); } $CI->load->model('charge_model'); $customer['customer_id'] = isset($customer['customer_id']) ? $customer['customer_id'] : FALSE; $order_id = $CI->charge_model->CreateNewOrder($client_id, $gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer_ip); $response = $CI->{$gateway_name}->Charge($client_id, $order_id, $gateway, $customer, $amount, $credit_card, $return_url, $cancel_url); // translate response codes into proper recurring terms if ($response['response_code'] == 1) { // set order OK $CI->charge_model->SetStatus($order_id, 1); $response['response_code'] = 100; $response['recurring_id'] = $subscription_id; } } else { // this is a free subscription if (date('Y-m-d', strtotime($start_date)) == date('Y-m-d')) { // create a $0 order for today's payment $CI->load->model('charge_model'); $customer['customer_id'] = isset($customer['customer_id']) ? $customer['customer_id'] : FALSE; $order_id = $CI->charge_model->CreateNewOrder($client_id, $gateway['gateway_id'], 0, $credit_card, $subscription_id, $customer['customer_id'], $customer_ip); $CI->charge_model->SetStatus($order_id, 1); $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id); } else { $response_array = array('recurring_id' => $subscription_id); } $response = $CI->response->TransactionResponse(100, $response_array); } if (isset($created_customer) and $created_customer == TRUE and $response['response_code'] != 100) { // charge was rejected, so let's delete the customer record we just created $CI->customer_model->DeleteCustomer($client_id, $customer['customer_id']); } elseif (isset($created_customer) and $created_customer == TRUE) { $response['customer_id'] = $customer['customer_id']; } if ($response['response_code'] != 100) { // clear it out completely $CI->recurring_model->DeleteRecurring($subscription_id); } if ($response['response_code'] == 100) { if (!empty($mark_as_renewed)) { $CI->recurring_model->SetRenew($mark_as_renewed, $subscription_id); // we used to only mark the old subscription as inactive, but now we will completely cancel it // this stops double recurring for PayPal-esque plugins that are initiated on their end $CI->recurring_model->CancelRecurring($client_id, $mark_as_renewed, TRUE); } if (!isset($response['not_completed']) or $response['not_completed'] == FALSE) { $CI->recurring_model->SetActive($client_id, $subscription_id); // delayed recurrings don't have a charge ID $response['charge_id'] = isset($response['charge_id']) ? $response['charge_id'] : FALSE; // trip it - were golden! TriggerTrip('new_recurring', $client_id, $response['charge_id'], $response['recurring_id']); // trip a recurring charge? if (!empty($response['charge_id'])) { TriggerTrip('recurring_charge', $client_id, $response['charge_id'], $response['recurring_id']); } if (!empty($coupon_id)) { // track coupon $CI->coupon_model->add_usage($coupon_id, $subscription_id, $response['charge_id'], $customer_id); } } else { unset($response['not_completed']); } } else { // did we require an IP address? if ($gateway_settings['requires_customer_ip'] == 1 and !$customer_ip) { die($this->response->Error(5019)); } } // pass back some values if ($charge_today === TRUE) { $response['amount'] = money_format("%!^i", $amount); } else { $response['amount'] = '0.00'; } $response['recur_amount'] = money_format("%!^i", $recur['amount']); $response['free_trial'] = (int) $free_trial; $response['start_date'] = date('Y-m-d', strtotime($start_date)); return $response; }
/** * Create a new recurring subscription. * * Creates a new recurring subscription and processes a charge for today. * * @param int $gateway_id The gateway ID to process this charge with * @param float $amount The amount to charge (e.g., "50.00") * @param array $credit_card The credit card information * @param int $credit_card['card_num'] The credit card number * @param int $credit_card['exp_month'] The credit card expiration month in 2 digit format (01 - 12) * @param int $credit_card['exp_year'] The credit card expiration year (YYYY) * @param string $credit_card['name'] The credit card cardholder name. Required only is customer ID is not supplied. * @param int $credit_card['cvv'] The Card Verification Value. Optional * @param int $customer_id The ID of the customer to link the charge to * @param array $customer An array of customer data to create a new customer with, if no customer_id * @param float $customer_ip The optional IP address of the customer * @param array $recur The details for a recurring charge * @param int $recur['plan_id'] The ID of the plan to pull recurring details from (Optional) * @param string $recur['start_date'] The start date of the subscription * @param string $recur['end_date'] The end date of the subscription * @param int $recur['free_trial'] The number of days to give a free trial before. Will combine with start_date if that is also set. (Optional) * @param float $recur['amount'] The amount to charge every INTERVAL days. If not there, the main $amount will be used. * @param int $recur['occurrences'] The total number of occurrences (Optional, if end_date doesn't exist). * @param string $recur['notification_url'] The URL to send POST updates to for notices re: this subscription. * @param string $return_url The URL for external payment processors to return the user to after payment * @param string $cancel_url The URL to send if the user cancels an external payment * @param int $renew The subscription that is being renewed, if there is one * @param int $coupon_id A potential coupon_id * * @return mixed Array with response_code and response_text */ function Recur($gateway_id, $amount = FALSE, $credit_card = array(), $customer_id = FALSE, $customer = array(), $customer_ip = FALSE, $recur = array(), $return_url = FALSE, $cancel_url = FALSE, $renew = FALSE, $coupon_id = 0) { $this->CI->load->library('field_validation'); // Get the gateway info to load the proper library $gateway = $this->GetGatewayDetails($gateway_id); if (!$gateway or $gateway['enabled'] == '0') { die($this->response->Error(5017)); } // load the gateway $gateway_name = $gateway['name']; $this->CI->load->library('billing/payment/' . $gateway_name); $gateway_settings = $this->{$gateway_name}->Settings(); $amount = (double) $amount; // validate function arguments if (!empty($amount) and empty($credit_card) and $gateway_settings['external'] == FALSE and $gateway_settings['no_credit_card'] == FALSE) { die($this->CI->response->Error(1004)); } $this->load->library('field_validation'); if (!empty($credit_card)) { // Validate the Credit Card number $credit_card['card_num'] = trim(str_replace(array(' ', '-'), '', $credit_card['card_num'])); $credit_card['card_type'] = $this->field_validation->ValidateCreditCard($credit_card['card_num'], $gateway); if (!$credit_card['card_type']) { die($this->response->Error(5008)); } if (!isset($credit_card['exp_month']) or empty($credit_card['exp_month'])) { die($this->response->Error(5008)); } if (!isset($credit_card['exp_year']) or empty($credit_card['exp_year'])) { die($this->response->Error(5008)); } } // are we linking this to another sub via renewal? if (!empty($renew)) { $this->CI->load->model('billing/recurring_model'); $renewed_subscription = $this->CI->recurring_model->GetSubscriptionDetails($renew); if (!empty($renewed_subscription)) { $mark_as_renewed = $renewed_subscription['subscription_id']; /** * this would automate the renewal subscription starting * after the renewed subscription * if (strtotime($renewed_subscription['next_charge_date']) > time()) { $recur['start_date'] = $renewed_subscription['next_charge_date']; } else { $recur['start_date'] = date('Y-m-d'); } $recur['free_trial'] = 0;*/ } else { $mark_as_renewed = FALSE; } } else { $mark_as_renewed = FALSE; } // Get the customer details if a customer id was included $this->load->model('billing/customer_model'); if (!empty($customer_id)) { $customer = $this->CI->customer_model->GetCustomer($customer_id); $customer['customer_id'] = $customer['id']; $created_customer = FALSE; } elseif (isset($customer) and !empty($customer)) { // look for embedded customer information // by Getting the customer after it's creation, we get a nice clean ISO2 code for the country $customer_id = $this->CI->customer_model->NewCustomer($customer); $customer = $this->CI->customer_model->GetCustomer($customer_id); $customer['customer_id'] = $customer_id; unset($customer_id); $created_customer = TRUE; } else { // no customer_id or customer information - is this a problem? // we'll check if this gateway required customer information if ($gateway_settings['requires_customer_information'] == 1) { die($this->response->Error(5018)); } // no customer information was passed but this gateway is OK with that, let's just get the customer first/last name // from the credit card name for our records if (!isset($credit_card['name'])) { die($this->response->Error(5004)); } else { $name = explode(' ', $credit_card['name']); $customer['first_name'] = $name[0]; $customer['last_name'] = $name[count($name) - 1]; $customer['customer_id'] = $this->CI->customer_model->SaveNewCustomer($customer['first_name'], $customer['last_name']); $created_customer = TRUE; } } // if we have an IP, we'll populate this field // note, if we get an error later: the first thing we check is to see if an IP is required // by checking this *after* an error, we give the gateway a chance to be flexible and, if not, // we give the end-user the most likely error response if (!empty($customer_ip)) { // place it in $customer array $customer['ip_address'] = $customer_ip; } else { $customer['ip_address'] = ''; } if (isset($recur['plan_id'])) { // we have a linked plan, let's load that information $this->CI->load->model('billing/plan_model'); $plan_details = $this->CI->plan_model->GetPlanDetails($recur['plan_id']); $interval = isset($recur['interval']) ? $recur['interval'] : $plan_details->interval; $notification_url = isset($recur['notification_url']) ? $recur['notification_url'] : $plan_details->notification_url; $free_trial = isset($recur['free_trial']) ? $recur['free_trial'] : $plan_details->free_trial; $occurrences = isset($recur['occurrences']) ? $recur['occurrences'] : $plan_details->occurrences; // calculate first charge amount: // 1) First charge is main $amount if given // 2) If no $recur['amount'], use plan amount // 3) Else use $recur['amount'] if (isset($amount)) { $amount = $amount; } elseif (isset($recur['amount'])) { $amount = $recur['amount']; } elseif (isset($plan_details->amount)) { $amount = $plan_details->amount; } $amount = $this->field_validation->ValidateAmount($amount); if ($amount === FALSE) { die($this->response->Error(5009)); } // store plan ID $plan_id = $plan_details->plan_id; } else { if (!isset($recur['interval']) or !is_numeric($recur['interval'])) { die($this->response->Error(5011)); } else { $interval = $recur['interval']; } // Check for a notification URL $notification_url = isset($recur['notification_url']) ? $recur['notification_url'] : ''; // Validate the amount if ($this->field_validation->ValidateAmount($amount) === FALSE) { die($this->response->Error(5009)); } $plan_id = 0; $free_trial = (isset($recur['free_trial']) and is_numeric($recur['free_trial'])) ? $recur['free_trial'] : FALSE; } // Validate the start date to make sure it is in the future if (isset($recur['start_date'])) { // adjust to server time $recur['start_date'] = server_time($recur['start_date'], 'Y-m-d', true); if (!$this->field_validation->ValidateDate($recur['start_date']) or $recur['start_date'] < date('Y-m-d')) { die($this->response->Error(5001)); } else { $start_date = date('Y-m-d', strtotime($recur['start_date'])); } } else { $start_date = date('Y-m-d'); } // do we have to adjust the start_date for a free trial? if ($free_trial) { $start_date = date('Y-m-d', strtotime($start_date) + $free_trial * 86400); } // get the next payment date if (date('Y-m-d', strtotime($start_date)) == date('Y-m-d')) { $next_charge_date = date('Y-m-d', strtotime($start_date) + $interval * 86400); } else { $next_charge_date = date('Y-m-d', strtotime($start_date)); } // if an end date was passed, make sure it's valid if (isset($recur['end_date'])) { // adjust to server time $recur['end_date'] = !isset($end_date_set_by_server) ? server_time($recur['end_date']) : $recur['end_date']; if (strtotime($recur['end_date']) < time()) { // end_date is in the past die($this->response->Error(5002)); } elseif (strtotime($recur['end_date']) < strtotime($start_date)) { // end_date is before start_date die($this->response->Error(5003)); } else { // date is good $end_date = date('Y-m-d', strtotime($recur['end_date'])); } } elseif (isset($occurrences) and !empty($occurrences)) { // calculate end_date from # of occurrences as defined by plan $end_date = date('Y-m-d', strtotime($start_date) + $interval * 86400 * $occurrences); } elseif (isset($recur['occurrences']) and !empty($recur['occurrences'])) { // calculate end_date from # of occurrences from recur node $end_date = date('Y-m-d', strtotime($start_date) + $interval * 86400 * $recur['occurrences']); } else { // calculate the end_date based on the max end date setting $end_date = date('Y-m-d', strtotime($start_date) + $this->config->item('max_recurring_days_from_today') * 86400); } if (!empty($credit_card) and isset($credit_card['exp_year']) and !empty($credit_card['exp_year'])) { // if the credit card expiration date is before the end date, we need to set the end date to one day before the expiration $check_year = $credit_card['exp_year'] > 2000 ? $credit_card['exp_year'] : '20' . $credit_card['exp_year']; $expiry = mktime(0, 0, 0, $credit_card['exp_month'], days_in_month($credit_card['exp_month'], $credit_card['exp_year']), $check_year); $date = strtotime($next_charge_date); while ($date < strtotime($end_date)) { if ($expiry < $date) { $end_date = date('Y-m-d', $date); break; } $date = $date + $interval * 86400; } } // adjust end date if it's less than next charge if (strtotime($end_date) <= strtotime($next_charge_date)) { // set end date to next charge $end_date = $next_charge_date; $total_occurrences = 1; } // figure the total number of occurrences $total_occurrences = round((strtotime($end_date) - strtotime($start_date)) / ($interval * 86400), 0); if ($total_occurrences < 1) { // the CC expiry date is only going to allow 1 charge $total_occurrences = 1; } // if they sent an $amount with their charge, this means that their first charge is different // so now we need to grab the true recurring amount, unless they overrode it if (isset($recur['amount'])) { $recur['amount'] = $recur['amount']; } elseif (is_object($plan_details) and isset($plan_details->amount)) { $recur['amount'] = $plan_details->amount; } else { $recur['amount'] = $amount; } // Save the subscription info $this->CI->load->model('billing/recurring_model'); $card_last_four = isset($credit_card['card_num']) ? substr($credit_card['card_num'], -4, 4) : '0'; $subscription_id = $this->CI->recurring_model->SaveRecurring($gateway['gateway_id'], $customer['customer_id'], $interval, $start_date, $end_date, $next_charge_date, $total_occurrences, $notification_url, $recur['amount'], $plan_id, $card_last_four, $coupon_id); // get subscription $subscription = $this->CI->recurring_model->GetRecurring($subscription_id); // is there a charge for today? $charge_today = date('Y-m-d', strtotime($subscription['date_created'])) == date('Y-m-d', strtotime($subscription['start_date'])) ? TRUE : FALSE; // set last_charge as today, if today was a charge if ($charge_today === TRUE) { $this->CI->recurring_model->SetChargeDates($subscription_id, date('Y-m-d'), $next_charge_date); } // if amount is greater than 0, we require a gateway to process if ($recur['amount'] > 0) { // recurring charges are not free $response = $this->CI->{$gateway_name}->Recur($gateway, $customer, $amount, $charge_today, $start_date, $end_date, $interval, $credit_card, $subscription_id, $total_occurrences, $return_url, $cancel_url); } elseif ($recur['amount'] <= 0 and $amount > 0) { // recurring charges are free, but there is an initial charge // can't be an external gateway if ($gateway_settings['external'] == TRUE) { die($this->response->Error(5024)); } // must have a start date of today if ($charge_today !== TRUE) { die($this->response->Error(5025)); } $this->CI->load->model('billing/charge_model'); $customer['customer_id'] = isset($customer['customer_id']) ? $customer['customer_id'] : FALSE; $order_id = $this->CI->charge_model->CreateNewOrder($gateway['gateway_id'], $amount, $credit_card, $subscription_id, $customer['customer_id'], $customer_ip); $response = $this->CI->{$gateway_name}->Charge($order_id, $gateway, $customer, $amount, $credit_card, $return_url, $cancel_url); // translate response codes into proper recurring terms if ($response['response_code'] == 1) { // set order OK $this->CI->charge_model->SetStatus($order_id, 1); $response['response_code'] = 100; $response['recurring_id'] = $subscription_id; } } else { // this is a free subscription if ($charge_today === TRUE) { // create a $0 order for today's payment $this->CI->load->model('billing/charge_model'); $customer['customer_id'] = isset($customer['customer_id']) ? $customer['customer_id'] : FALSE; $order_id = $this->CI->charge_model->CreateNewOrder($gateway['gateway_id'], 0, $credit_card, $subscription_id, $customer['customer_id'], $customer_ip); $this->CI->charge_model->SetStatus($order_id, 1); $response_array = array('charge_id' => $order_id, 'recurring_id' => $subscription_id); } else { $response_array = array('recurring_id' => $subscription_id); } $response = $this->CI->response->TransactionResponse(100, $response_array); } if (isset($created_customer) and $created_customer == TRUE and $response['response_code'] != 100) { // charge was rejected, so let's delete the customer record we just created $this->CI->customer_model->DeleteCustomer($customer['customer_id']); } elseif (isset($created_customer) and $created_customer == TRUE) { $response['customer_id'] = $customer['customer_id']; } if ($response['response_code'] != 100) { // clear it out completely $this->CI->recurring_model->DeleteRecurring($subscription_id); } if ($response['response_code'] == 100) { // save the "mark_as_renewed" subscription as charge data so we can do this maintenance later // we no longer do this maintenance here because for external gateways, we need to wait // for the user to complete their payment if (!empty($mark_as_renewed)) { $this->CI->load->model('billing/charge_data_model'); $this->CI->charge_data_model->Save('r' . $subscription_id, 'mark_as_renewed', $mark_as_renewed); } if (!isset($response['not_completed']) or $response['not_completed'] == FALSE) { $this->CI->recurring_model->SetActive($subscription_id); // delayed recurrings don't have a charge ID $response['charge_id'] = isset($response['charge_id']) ? $response['charge_id'] : FALSE; // hook call $this->app_hooks->data('subscription', $response['recurring_id']); if (!empty($mark_as_renewed)) { $this->app_hooks->trigger('subscription_renew', $response['recurring_id']); } else { $this->app_hooks->trigger('subscription_new', $response['recurring_id']); } // trip a recurring charge? if (!empty($response['charge_id'])) { // hook $this->app_hooks->data('invoice', $response['charge_id']); $this->app_hooks->trigger('subscription_charge', $response['charge_id'], $response['recurring_id']); } } else { unset($response['not_completed']); } } else { // did we require an IP address? if ($gateway_settings['requires_customer_ip'] == 1 and !$customer_ip) { die($this->response->Error(5019)); } } $this->app_hooks->reset(); return $response; }