开发者

PayPal IPN, lots of sign ups, one payment, all in the wrong order

开发者 https://www.devze.com 2023-02-08 03:39 出处:网络
I\'ve recently implemented a PayPal IPN into CodeIgniter2, using the PayPal Lib.I\'m using the system for subscriptions.

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.

0

精彩评论

暂无评论...
验证码 换一张
取 消