Related
QUESTION
I get an ERROR 3370 from Sage Pay when trying to repeat transactions of type AUTHORISE (but not PAYMENT). I am using Server Integration with PHP/cURL.
ANSWER
This is most likely because you need to send CoF values during your initial AUTHENTICATE transaction. This establishes the initial record as a valid "Credentital on File" and allows you to carry out REPEAT transactions later.
In a nutshell, the initial AUTHENTICATE record must be created with:
COFUsage = FIRST
InitiatedType = CIT
MITType = UNSCHEDULED
HERE IS THE FULL PHP/CURL CODE THAT IS NOW WORKING FOR ME:
STEP 1: AUTHENTICATE
Assuming you are using Server Integration and the customer is "in session" your initial cURL code will look something like this:
// Initialise cURL
$curl = curl_init();
// Set the Sage Pay server URL
$serverLive="https://live.sagepay.com/gateway/service/authorise.vsp";
// Connect to the Sage Pay server
curl_setopt($curl, CURLOPT_URL, $serverLive);
// Set cURL to post variables
curl_setopt($curl, CURLOPT_POST, 1);
// Declare account variables for posting to the Sage Pay server
$VPSProtocol="4.0"; // Must be 4.0 for CoF usage
$Vendor="......."; // Your Sage Pay Vendor Name
// Declare product variables for posting to the Sage Pay server
$Amount=0.01; // This transaction will be for 1 pence
$Currency="GBP";
$Description="......."; // Product description
// Declare URL of your callback page for posting to the Sage Pay server
$NotificationURL="https://www.......";
// Create a unique 16-character VendorTxCode for posting to the Sage Pay server
$UserID=9999; // Unique user ID
$salt="d5s63ffd6s7fgdhs55377yrwesr24553"; // Encryption key
$VendorTxCode=substr(strtoupper(md5(date("U").$UserID.$salt)), 0, 16);
// Declare other variables to pass to Sage Pay (ie. customer name, email, billing address etc)
// These will have been entered via a form on your website
$CustomerName=".......";
$CustomerEmail=".......";
$BillingFirstnames=".......";
$BillingSurname=".......";
$BillingAddress1=".......";
$BillingCity=".......";
$BillingPostCode=".......";
$BillingCountry="GB";
$BillingPhone=".......";
$DeliveryFirstnames=".......";
$DeliverySurname=".......";
$DeliveryAddress1=".......";
$DeliveryCity=".......";
$DeliveryPostCode=".......";
$DeliveryCountry="GB";
$DeliveryPhone=".......";
Now is probably a good time to INSERT these variables into your MySQL database. Create a table called "sagepay" with field/values as follows:
userID = $UserID
VendorTxCode = $VendorTxCode
TxType = AUTHENTICATE
Amount = $Amount
Description = $Description
You should also have the following fields in your sagepay database table. These will start off empty and will be UPDATE(d) later:
SecurityKey
VPSTxId
TxAuthNo
RelatedVendorTxCode
RelatedSecurityKey
RelatedVPSTxId
RelatedTxAuthNo
SchemeTraceID
ACSTransID
DSTransID
Status
Now post your variables to the Sage Pay server via cURL. Your cURL post should send a $COFUsage value of FIRST to correspond with this being an initial AUTHENTICATE transaction and $InitiatedType must be set to CIT to indicate the customer is "in session" (ie. they are going to enter their payment details on your website in a moment):
// Post the variables to the Sage Pay server
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query(array('Profile' => 'LOW', 'VPSProtocol' => $VPSProtocol, 'Vendor' => $Vendor, 'TxType' => 'AUTHENTICATE', 'VendorTxCode' => $VendorTxCode, **'Apply3DSecure' => '1', 'COFUsage' => 'FIRST', 'InitiatedType' => 'CIT',** 'Amount' => $Amount, 'Currency' => $Currency, 'Description' => $Description, 'CustomerName' => $CustomerName, 'CustomerEMail' => $CustomerEmail, 'BillingFirstnames' => $BillingFirstnames, 'BillingSurname' => $BillingSurname, 'BillingAddress1' => $BillingAddress1, 'BillingCity' => $BillingCity, 'BillingPostCode' => $BillingPostCode, 'BillingCountry' => $BillingCountry, 'BillingPhone' => $BillingPhone, 'DeliveryFirstnames' => $DeliveryFirstnames, 'DeliverySurname' => $DeliverySurname, 'DeliveryAddress1' => $DeliveryAddress1, 'DeliveryCity' => $DeliveryCity, 'DeliveryPostCode' => $DeliveryPostCode, 'DeliveryCountry' => $DeliveryCountry, 'DeliveryPhone' => $DeliveryPhone, 'NotificationURL' => $NotificationURL', 'Status' => 'OK'
)));
// This is supposed to speed things up (not sure if it does!)
curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
// Request a response from cURL
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
The above code will create an AUTHENTICATE record on the Sage Pay server and return a cURL string containing values that you will use to UPDATE your previously created database record. These variables will be reused when you create the AUTHORISE transaction (see Step 2):
// Get server response
$response = curl_exec($curl);
// Close cURL
curl_close ($curl);
Convert the $response string into an array called $results[]:
// Convert $response string into an array called $results[]
$results = [];
foreach (explode("\n", $response) as $line)
{
list ($key, $value) = explode('=', $line, 2);
$results[$key] = trim($value); // Trim to remove white space
}
The following variables are contained in the $results[] array. Make sure you UPDATE your initial AUTHENTICATE database record with these values. You will need to recall them when you come to AUTHORISE the transaction and take the first payment:
$SecurityKey=$results['SecurityKey']; // Save this to your database
$VPSTxId=$results['VPSTxId']; // Save this to your database
$results[] will also contain the following variable:
$NextURL=$results['NextURL'];
Once an AUTHENTICATE record is created on the Sage Pay server, you can display a card payment page to your customer using an iFrame with the source URL set to $NextURL
<iframe name="my_iframe" src="<?= $NextURL ?>" width='100%' height='520'></iframe>
At this point the customer will enter their card details and, if everything is in order, the transaction will be completed and the iFrame will update with your $NotificationURL
Sage Pay will pass the following variables to your $NotificationURL in the $_REQUEST[] array. It is a good idea to "trim" these values to ensure no white space creeps in:
$Status=trim($_REQUEST['Status']); // This should be "OK"
$TxAuthNo=trim($_REQUEST['TxAuthNo']);
$ACSTransID=trim($_REQUEST['ACSTransID']);
$DSTransID=trim($_REQUEST['DSTransID']);
UPDATE your "sagepay" database table with these values and then display your "Thank You" message.
STEP 2: AUTHORISE
Now you have an AUTHENTICATE record on the Sage Pay server, and a corresponding record in your MySQL database table, you must AUTHORISE the transaction to take the initial payment. This involves creating a new AUTHORISE record on the Sage Pay server and in your MySQL database.
First, create a new 16-character $VendorTxCode for the AUTHORISE record.
Then, SELECT and transfer the values from the original AUTHENTICATE record into "related" variables for resubmission to Sage Pay:
// Declare "related" variables
$RelatedVendorTxCode = $VendorTxCode; // The original VendorTxCode you created
$RelatedVPSTxId = $VPSTxId; // The $VPSTxId returned in $results[] array
$RelatedSecurityKey = $SecurityKey; // The $SecurityKey returned in $results[] array
Now is a good time to INSERT the $VendorTxCode and these "related" variables as a new record in your MySQL database. Set the TxType field for this new record to AUTHORISE.
Next, pass your variables to Sage Pay via cURL to create an AUTHORISE record on the Sage Pay server. Note, the $InitiatedType is now MIT to indicate the customer is no longer "in session", but the $COFUsage value still needs to be FIRST:
// Create an AUTHORISE record
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query(array('VPSProtocol' => $VPSProtocol, 'VendorTxCode' => $VendorTxCode, 'Vendor' => $Vendor, 'TxType' => 'AUTHORISE', **'Apply3DSecure' => '1', 'COFUsage' => 'FIRST', 'InitiatedType' => 'MIT', 'MITType' => 'UNSCHEDULED',** 'Amount' => $Amount, 'Description' => $Description, 'RelatedVPSTxId' => $RelatedVPSTxId, 'RelatedVendorTxCode' => $RelatedVendorTxCode, 'RelatedSecurityKey' => $RelatedSecurityKey, 'RelatedTxAuthNo' => $RelatedTxAuthNo)));
Sage Pay will respond again with a string ($response). Convert $response into a $results[] array as before. These are some of the variables you will end up with:
$TxAuthNo
$VPSTxId
$SecurityKey
$SchemeTraceID
$ACSTransID
$DSTransID
$Status
UPDATE the AUTHORISE record in your MySQL database table with these variables. You will need these variables for when you come to REPEAT the transaction.
$TxAuthNo is an authorisation code which is only returned if the AUTHORISE record is successfully created on the Sage Pay server (ie. if $Status is "OK").
$SchemeTraceID is your CoF "token" for repeating successful AUTHORISE transactions.
STEP 3: REPEAT
You can REPEAT against AUTHORISE transactions using the following cURL code. Note, the $COFUsage value now changes from FIRST to SUBSEQUENT and you must send the $SchemeTraceID (token) to verify the transaction as a valid "Credential on File". The "related" variables are those of the original AUTHORISE record (ie. $VendorTxCode, $TxAuthNo, $VPSTxId, and $SecurityKey):
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query(array('VPSProtocol' => $VPSProtocol, 'Vendor' => $Vendor, 'TxType' => 'REPEAT', 'VendorTxCode' => $VendorTxCode, **'Apply3DSecure' => '1', 'COFUsage' => 'SUBSEQUENT', 'InitiatedType' => 'MIT', 'MITType' => 'UNSCHEDULED', 'SchemeTraceID'=> $SchemeTraceID,** 'Amount' => $Amount, 'Currency' => $Currency, 'Description' => $Description, 'RelatedVPSTxId' => $RelatedVPSTxId, 'RelatedVendorTxCode' => $RelatedVendorTxCode, 'RelatedSecurityKey' => $RelatedSecurityKey, 'RelatedTxAuthNo' => $RelatedTxAuthNo )));
I was also facing same issue. But I have used this library and reason behind this issue is in the first transaction request they(Sagepay server) are getting blank COFUsage, InitiatedType, MITType params
https://github.com/thephpleague/omnipay-sagepay
I installed 4.0 version of omnipay-sagepay library rather then 3.0
{
"require": {
"omnipay/sagepay": "~4.0"
}
}
and in the first transaction, I have passed these extra parameters.
$gateway->setCOFUsage("FIRST");
$gateway->setInitiatedType("CIT");
$gateway->setMITType("UNSCHEDULED");
And for repeat transactions, I have passed these params
$gateway->setCOFUsage('SUBSEQUENT');
$gateway->setInitiatedType('MIT');
$gateway->setMITType('UNSCHEDULED');
$gateway->setSchemeTraceID($TransactionReference['scheme_trace_Id']);
and it's working fine for me. Response from sagepay team
i was implementing stripe payment in testing mode.Here i got an error like Same token is used again. is there any different way to add multiple cards.
or i need to call retrive function in a separate page so that conflict of same token never come again.And how should i set a card default.
public function createToken($data)
{
$TokenResult=Token::create(array(
"card" => array(
"name" => $data['name'],
"number" => $data['card_number'],
"exp_month" => $data['month'],
"exp_year" => $data['year'],
"cvc" => $data['cvc']
)));
//echo "<pre>";;
//print_r($TokenResult);
$this->token=$TokenResult['id'];//store token id into token variable
$this->chargeCard($this->token); //call chargecard function via passing token id
}
/*
* function to create customer
*/
public function createCustomer($data,$token=null)//pass form data and token id
{
$customer=Customer::create(array(
"email"=>$data['email'],
"description" => $data['name'],
"source" => $token // obtained with Stripe.js
));
$customerId=$customer['id'];
$this->retriveCustomer($customerId,$token);
}
/*
* function to retrive current customers for adding multiple cards to same customers*/
public function retriveCustomer($customerid,$token)
{
echo $this->token;
//die('here');
$retriveResult=Customer::retrieve($customerid);
$retriveResult->sources->create(array("source" =>$this->token));
return $retriveResult;
}
First, please note that unless you are PCI certified and allowed to directly manipulate card data, you should never have access to card numbers in your server-side code.
Card tokens should be created client-side, via Checkout or Elements. Your server should only deal with client-side created card tokens and never with PCI-sensitive information (card numbers and CVCs). This will greatly decrease the burden of PCI compliance and make you eligible for PCI SAQ A.
In PHP, this is how you'd add a card to an existing customer object:
$customer = \Stripe\Customer::retrieve("cus_...");
$card = $customer->sources->create(array(
"source" => $token // token created by Checkout or Elements
));
I think you dont need to create a token in case of adding new card. It helps while you update certain card. So the flow of addition will be same as you created for first card.
I dont know which stripe version you are using, I am using bit old:
$card_array = array(
'card' => array(
'number' => $number,
'exp_month' => $exp_month,
'exp_year' => $exp_year,
'cvc' => $cvc,
'name' => $name
)
);
$card_obj = $this->setData("\Stripe\Customer", "retrieve", $customer_id, TRUE);
$card = $card_obj->sources->create($card_array);
Stripe's docs don't explain a lot of the more nuanced procedures so you have to do a lot of testing.
Assuming you have a customer object $cu, with the token you get from checkout or whatever you use:
$card = $cu->sources->create(['source'=>$token]);
will add a card to the customer. It just adds the card to the list; subsequent calls will add cards to the list. Note that it does not check for duplicates, so the same card can be on the list multiple times. It will also not set the new card to the active card. To make a card the default (or active), use
$cu->default_source = $card
$cu->save();
Using the older card interface:
$cu->card = $token;
$cu->save();
The new card will replace the default card. It will NOT make the previously default card inactive; it will delete the current default and make the new card the active default. The card interface is the easiest if you're just allowing 1 card to be attached to a customer at a time.
I am currently working on a submit order API for OpenCart. The purpose of this API is not for the webpage part of OpenCart, but for our mobile app to interact with OpenCart.
From my research, there appears to be two ways to submit an order:
Method One:
On the OpenCart webpage, when you want to physically want to submit an order, you go through the workflow of:
add product to shopping cart through checkout/cart/add
input payment_address through checkout/payment_address/validate
input shipping_address through checkout/shipping_address/validate
input shipping method through checkout/shipping_method/validate
input payment method through checkout/payment_method/validate
Confirm/submit the order through (I don't know what URL is actually requested to submit an order).
It appears the benefit of this method is that, given the condition that the customer is logged in, AND you have an API to add items to cart, you don't need to go through the step of manually adding products to cart in the URL. You can also use a string such as existing in payment_address and shipping_address to denote an existing address (so you don't need to manually input anything at all in the URL).
Method Two:
Using checkout/manual. This method, however, requires fully manually setting the inputs for everything:
store_id
customer_id
customer_group_id
payment_country_id
payment_zone_id
payment_country_id
order_product[]
order_voucher
shipping_method[]
order_total[contains keys such as code, title, text, value, and sort_order]
payment[]
May have missed something..
The use of checkout/manual seems to be the API that is for our needs, and I especially like that it would return warning/error messages if the request is not successful - however, the first method allows the the customer to have an interface to add products to cart and access existing variables such as shipping_address, payment_address, and more, given the condition that they're logged in (I'm aware that there is code that does cart->clear() and customer->logout() if the user is logged in).
My question is - has anyone ever implemented manually checking out before? How did you implement it?
Update:
Currently writing a script that would POST some dummy inputs to checkout/manual:
public function submit()
{
# Our new data
$data = array(
"store_id" => 0,
"customer" => "My Name",
"customer_id" => 1,
"customer_group_id" => 1,
"payment_country_id" => 223,
"payment_zone_id" => 3663,
"order_product[0][product_id]" => 1791,
"order_product[0][quantity]" => 1,
"order_product[0][price]" => 5.01,
"order_total[0][code]" => "sub_total",
"order_total[0][title]" => "Sub-Total",
"order_total[0][text]" => "$5.01",
"order_total[0][value]" => 5.01,
"order_total[0][sort_order]" => 1,
"order_total[1][code]" => "total",
"order_total[1][title]" => "Total",
"order_total[1][text]" => "$5.01",
"order_total[1][value]" => 5.01,
"order_total[1][sort_order]" => 9,
"payment_firstname" => "My",
"payment_lastname" => "Name",
"payment_company" => "SomeCompany",
"payment_address_1" => "#123 1234 NameOf street",
"payment_address_2" => "",
"payment_postcode" => "12345",
"payment_city" => "New York",
"shipping_firstname" => "My",
"shipping_lastname" => "Name",
"shipping_company" => "SomeCompany",
"shipping_address_1" => "#123 1234 NameOf street",
"shipping_address_2" => "",
"shipping_postcode" => "12345",
"shipping_city" => "New York",
"shipping" => "free.free",
"shipping_method" => "Free Shipping",
"shipping_code" => "free.free",
"shipping_country_id" => 223,
"shipping_zone_id" => 3663,
"payment" => "cod",
"payment_method" => "Cash On Delivery",
"payment_code" => "cod",
"order_status_id" => 1
);
# Create a connection
$url = HTTP_SERVER . 'index.php?route=checkout/manual';
$ch = curl_init($url);
# Form data string
$postString = http_build_query($data, '', '&');
# Setting our options
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postString);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
# Get the response
$response = curl_exec($ch);
echo $response;
curl_close($ch);
}
However, the return data is this:
{"error":{"warning":"You do not have permission to access this page, please refer to your system administrator."}}
Update 2:
The error was thrown since I could not satisfy either of these conditions when callling checkout/manual: $this->user->isLogged() && $this->user->hasPermission('modify', 'sale/order')
I temporarily removed it by replace it with true so I can always manually add an order.
Using my input params, I do get this JSON:
"success":"Order totals has been successfully re-calculated!", however, checking the administrative orders list, it does not appear a new order gets added.
Update 3:
Despite the ability to create the order, I don't know how to submit the order.
First of all, it is dangerous to comment out the code $this->user->isLogged() && $this->user->hasPermission('modify', 'sale/order'). Instead add some kind of override to the parts that reference $this->request->post['customer_id'] so that you set the post data based on actual customer id. If not, it would be possible for regular customers to spoof log in and checkout data as another customer simply by sending whatever POST data they want.
In regards to the actual order creation, you'll note that checkout/manual returns a json with all necessary order data. All you need to do is add something like this after if (!isset($json['error'])) {
$this->load->model('checkout/order');
$json['order_id'] = $this->model_checkout_order->addOrder($json);
Then return the json to your mobile app and show a confirmation or do whatever you want with the order data. It might also be helpful to send a unique mobile app key along the validate whether or not the request is allowed and/or link to the appropriate customer account.
I have used the Omnipay PayPal_Express checkout script on my site and everything works fine when I pay for an order except the order doesn't show in the PayPal Sandbox account.
It does show when I use the same script for PayPal_Pro.
My code is as follows:
use Omnipay\Omnipay;
// PayPal Express:
if(isset($_POST['paypalexpress'])) {
$gateway = GatewayFactory::create('PayPal_Express');
$gateway->setUsername('{myusername}');
$gateway->setPassword('{mypassword}');
$gateway->setSignature('{mysignauture}');
$gateway->setTestMode(true);
$response = $gateway->purchase(
array(
'cancelUrl'=>'http://www.mysite.com/?cancelled',
'returnUrl'=>'http://www.mysite.com/?success',
'amount' => "12.99",
'currency' => 'GBP',
'Description' => 'Test Purchase for 12.99'
)
)->send();
$response->redirect();
}
I have created two test accounts in my Sandbox, one is for the above API and one I use to pay with. I have tried paying with the test card details and the login but the order detail doesn't show in the account.
Can anyone help?
It looks like you're missing the completePurchase() part when Paypal returns to your returnUrl. My code assumes that you have the order details in a variable $order, but it may look something like this:
if(isset($_GET['success'])) {
$response = $gateway->completePurchase(array(
'transactionId' => $order->transaction,
'transactionReference' => $order->reference,
'amount' => $order->total,
'currency' => $order->currency,
))->send();
if ( ! $response->isSuccessful())
{
throw new Exception($response->getMessage());
}
}
Let me know if you need any help retrieving the order details on return. It can be stored in a session before you redirect, or in a database. If you haven't done already, take a look at the example code: https://github.com/omnipay/example/blob/master/index.php
I've recently implemented a PayPal IPN into CodeIgniter2, using the PayPal Lib. I'm using the system for subscriptions.
I have a table in my database that records all IPN requests in the database.
For some reason, after every sign up the IPN requests aren't coming through properly. I tend to get one subscr_payment along with several subscr_signups, all with the same subscr_id. It's causing untolds amount of hassle within the system, for obvious reasons. What adds to this, is the fact that the IPN requests don't come in the correct order, sometimes I get the subscr_payment before the subscr_signup - making it impossible to track as there's no subscr_id from the sign up to link it to a user.
I've had a Google and can't find much on this, I seem to be a little bit of an anomaly. I'm wondering if it's something to do with the PayPal Lib I'm using, but I don't really want to have to do it outside of CodeIgniter, as I am doing a lot of processing. Below is the full IPN script.
class Paypal extends CI_Controller {
function _construct()
{
parent::_construct();
$this->load->library('paypal_lib');
}
function ipn()
{
$this->output->enable_profiler(TRUE);
$this->load->model('payments_model');
$this->load->model('paypal_model');
$this->load->model('users_model');
ob_start();
if ($this->paypal_lib->validate_ipn())
{
$paypal_id = $this->paypal_model->add_paypal_ipn($this->paypal_lib->ipn_data);
// Split the 'custom' field up, containing ID of temp user, ID of package and coupon
$custom = explode(';', $this->paypal_lib->ipn_data['custom']);
###
# subscription sign up
###
if($this->paypal_lib->ipn_data['txn_type'] == 'subscr_signup') {
// Activate user/move from temp > live
$this->users_model->move_temp($custom[0], $this->paypal_lib->ipn_data['subscr_id']);
} # end subscr_signup
###
# subscription payment
###
if($this->paypal_lib->ipn_data['txn_type'] == 'subscr_payment') {
// Grab the coupon info, if we have one
$discount = 1;
if(!empty($custom[2])){
$this->load->model('coupons_model');
$couponinfo = $this->coupons_model->get_coupon($custom[2]);
$discount = $couponinfo->discount;
}
// Grab the package info
$package = $this->packages_model->get_package($custom[1]);
$price = $package->monthly * $discount; // Calculate discount, 0.8 = 20% off
// Does the price calculated match the gross price? If not something fishy is going on, block it
if($price != $this->paypal_lib->ipn_data['mc_gross']){
mail(CONTACT_EMAIL, SITE_NAME.' failed payment attempt, possible hack', 'Price paid doesnt match price computed... paid: '.$this->paypal_lib->ipn_data['mc_gross'].' - price worked out: '.$price."\n\n".print_r($this->paypal_lib->ipn_data, true));
exit;
}
// Grab the user's details based on the subscr_id
$user = $this->users_model->get_user_by_subscr_id($this->paypal_lib->ipn_data['subscr_id']);
// Add payment to the payments table
$data = array(
'user_id' => $user->user_id,
'subscr_id' => $user->subscr_id,
'txn_id' => $this->paypal_lib->ipn_data['txn_id'],
'amount' => $this->paypal_lib->ipn_data['mc_gross'],
'package_id' => $custom[1],
'coupon' => (empty($custom[2]) ? '' : $custom[2])
);
$this->payments_model->add_payment($data);
// Set (forced) user as active, and update their current active package
$data1 = array(
'package_id' => $custom[1],
'active' => 1
);
$this->users_model->update_user($data1, $user->user_id);
} # end subscr_payment
###
# subscription failed/cancelled
###
if($this->paypal_lib->ipn_data['txn_type'] == 'subscr_cancel' || $this->paypal_lib->ipn_data['txn_type'] == 'subscr_failed') {
// Grab user
$user = $this->users_model->get_user_by_subscr_id($this->paypal_lib->ipn_data['subscr_id']);
// Make user inactive
$data = array('active' => 0);
$this->users_model->update_user($data, $user->user_id);
} # end subscr_cancel|subscr_failed
###
# subscription modified/payment changed
###
if($this->paypal_lib->ipn_data['txn_type'] == 'subscr_modify') {
// Grab the coupon info, if we have one
$discount = 1;
if(!empty($custom[2])){
$this->load->model('coupons_model');
$couponinfo = $this->coupons_model->get_coupon($custom[2]);
$discount = $couponinfo->discount;
}
// Grab the package info
$package = $this->packages_model->get_package($custom[1]);
$price = $package->monthly * $discount; // Calculate discount, 0.8 = 20% off
// Does the price calculated match the gross price? If not something fishy is going on, block it
if($price != $this->paypal_lib->ipn_data['mc_gross']){
mail(CONTACT_EMAIL, SITE_NAME.' failed payment attempt, possible hack', 'Price paid doesnt match price computed... paid: '.$this->paypal_lib->ipn_data['mc_gross'].' - price worked out: '.$price."\n\n".print_r($this->paypal_lib->ipn_data, true));
exit;
}
// Grab the user's details based on the subscr_id
$user = $this->users_model->get_user_by_subscr_id($this->paypal_lib->ipn_data['subscr_id']);
// Add payment to the payments table
$data = array(
'user_id' => $user->user_id,
'subscr_id' => $user->subscr_id,
'txn_id' => $this->paypal_lib->ipn_data['txn_id'],
'amount' => $this->paypal_lib->ipn_data['mc_gross'],
'package_id' => $custom[1],
'coupon' => (empty($custom[2]) ? '' : $custom[2])
);
$this->payments_model->add_payment($data);
// Set (forced) user as active, and update their current active package
$data1 = array(
'package_id' => $custom[1],
'active' => 1
);
$this->users_model->update_user($data1, $user->user_id);
} # end subscr_modify
}
}
Below is an example of the calls made to my IPN for each transaction (CSV).
paypal_id,txn_id,subscr_id,txn_type,created
1,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:19:43
2,9XM95194MM564230E,I-FMUK0B5KJWKA,subscr_payment,2011-02-03 16:19:45
3,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:19:57
4,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:20:19
6,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:21:03
7,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:22:25
8,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:25:08
10,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:30:33
12,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 16:41:16
14,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 17:02:42
16,NULL,I-FMUK0B5KJWKA,subscr_signup,2011-02-03 17:45:26
Consider this - PayPal is insert profanity. Now revisit the problem.
The chances are this isn't your fault, or CodeIgniter's or the Library's. PayPal is very bad at giving data in a uniform and timely manner, it is also slow and doesn't link data together very well.
My advice to you is save everything into an IPN table whenever a callback is made, even email yourself when ever an IPN call is made. Then work to try and figure out what PayPal is actually sending you, what you want and throw out the rest.
I think an IPN call is made even if the transaction has nothing to do with your web site. So if your Grandma sends you your Christmas money via PayPal it'll appear on the IPN callback.
Hope that helps a bit.
paypal isn't exactly easy to use but let me share 3 tips totackle the problems you are facing.
1) Create a table to store all IPN response from PayPal. Make sure you have a column called "raw" that stores EVERYTHING... do "json_encode($this->paypal_lib->ipn_data)". This will save you... since you can later write a script to pull out data from the raw column into it's own column down the road. This also helps with debugging.
2) For a start just pull out what is necessary out into columns of the ipn table so you can query easily... here's everything I've deem necessary for my use case which is probably similar to yours.
$this->payment_model->create_ipn(array(
'invoice' => $this->paypal_lib->ipn_data['invoice'],
'txn_type' => $this->paypal_lib->ipn_data['txn_id'],
'parent_txn_id' => $this->paypal_lib->ipn_data['parent_txn_id'],
'txn_type' => $this->paypal_lib->ipn_data['txn_type'],
'item_name' => $this->paypal_lib->ipn_data['item_name'],
'item_number' => $this->paypal_lib->ipn_data['item_number'],
'quantity' => $this->paypal_lib->ipn_data['quantity'],
'exchange_rate' => $this->paypal_lib->ipn_data['exchange_rate'],
'settle_amount' => $this->paypal_lib->ipn_data['settle_currency'],
'settle_amount' => $this->paypal_lib->ipn_data['settle_amount'],
'mc_currency' => $this->paypal_lib->ipn_data['mc_currency'],
'mc_fee' => $this->paypal_lib->ipn_data['mc_fee'],
'mc_gross' => $this->paypal_lib->ipn_data['mc_gross'],
'payment_date' => $this->paypal_lib->ipn_data['payment_date'],
'payment_status' => $this->paypal_lib->ipn_data['payment_status'],
'payment_type' => $this->paypal_lib->ipn_data['payment_type'],
'pending_reason' => $this->paypal_lib->ipn_data['pending_reason'],
'reason_code' => $this->paypal_lib->ipn_data['reason_code'],
'subscr_id' => $this->paypal_lib->ipn_data['subscr_id'],
'subscr_date' => $this->paypal_lib->ipn_data['subscr_date'] ? mdate('%Y-%m-%d %H:%i:%s', strtotime($this->paypal_lib->ipn_data['subscr_date'])) : NULL,
'subscr_effective' => $this->paypal_lib->ipn_data['subscr_effective'] ? mdate('%Y-%m-%d %H:%i:%s', strtotime($this->paypal_lib->ipn_data['subscr_effective'])) : NULL,
'period1' => $this->paypal_lib->ipn_data['period1'],
'period2' => $this->paypal_lib->ipn_data['period2'],
'period3' => $this->paypal_lib->ipn_data['period3'],
'amount1' => $this->paypal_lib->ipn_data['amount1'],
'amount2' => $this->paypal_lib->ipn_data['amount2'],
'amount3' => $this->paypal_lib->ipn_data['amount3'],
'mc_amount1' => $this->paypal_lib->ipn_data['mc_amount1'],
'mc_amount2' => $this->paypal_lib->ipn_data['mc_amount2'],
'mc_amount3' => $this->paypal_lib->ipn_data['mc_amount3'],
'recurring' => $this->paypal_lib->ipn_data['recurring'],
'reattempt' => $this->paypal_lib->ipn_data['reattempt'],
'retry_at' => $this->paypal_lib->ipn_data['retry_at'] ? mdate('%Y-%m-%d %H:%i:%s', strtotime($this->paypal_lib->ipn_data['retry_at'])) : NULL,
'recur_times' => $this->paypal_lib->ipn_data['recur_times'],
'payer_id' => $this->paypal_lib->ipn_data['payer_id'],
'payer_email' => $this->paypal_lib->ipn_data['payer_email'],
'payer_status' => $this->paypal_lib->ipn_data['payer_status'],
'payer_business_name' => $this->paypal_lib->ipn_data['payer_business_name'],
'ipn_track_id' => $this->paypal_lib->ipn_data['ipn_track_id'],
'raw' => json_encode($this->paypal_lib->ipn_data_arr),
'test_ipn' => $this->paypal_lib->ipn_data['test_ipn']
));
don't copy my code above since it's only meant to give you some rough ideas... if you do adapt my code do also ensure ipn_data function is like this (else you will get tons of errors)
function ipn_data($key)
{
return isset($this->fields[$key]) ? $this->fields[$key] : NULL;
}
to understand all the possible stuff they can send back this link is gold
https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_html_IPNandPDTVariables
but ^sigh^ don't trust it to be updated. i have found inconsistencies in what they doc says and what they sent back to me.
3) OK this i have to admit is another silly thing that paypal does - they don't give you an IPN date even tho' they don't guarantee the order in which it arrives at your server. For subscr_payment they give you payment_date... for subscr_signup they give you subscr_date... so what you need to do to get your IPN in the correct order is to have a column called ipn_date.
'ipn_date' => isset($this->paypal_lib->ipn_data['payment_date']) ?
mdate('%Y-%m-%d %H:%i:%s', strtotime($this->paypal_lib->ipn_data['payment_date'])) :
(isset($this->paypal_lib->ipn_data['subscr_date']) ?
mdate('%Y-%m-%d %H:%i:%s', strtotime($this->paypal_lib->ipn_data['subscr_date'])) : NULL),
now all is cool, you can "order by ipn_date" and I assure you everything will be in the correct order.
p.s. note the my first example code doesn't have this column, but it IS meant to be there. i'm just copying and pasting my development code to give you an idea.
What I do is ignore the signup ones and just process (create new user etc) on the actual payment transaction. And I wouldn't bother storing all those IPN trans. Have your IPN script send you an EMail on every one of them though, with an echo of all the fields posted. Then you'll have a record of them.