I'm using a Wordpress REST API for building a mobile app and did heavy customizing function on ( post type ) wooCommerce to be specific that makes the response time big when requesting, for example, an endpoint like this /wp-json/wc/v3/products
my customization is registering new fields on product post type.
What I need is checking if is it a single record like this?
/wp-json/wc/v3/products/123456
Or fetching all products like this?
/wp-json/wc/v3/products
My php code for register new fields:
add_action('rest_api_init','get_custom_field');
function get_custom_field() {
register_rest_field('product', 'custom_variations', array(
'get_callback' => 'custom_variations'
));
register_rest_field('product', 'components', array(
'get_callback' => 'product_components'
));
}
You can add filter to check if there's an id provided in the url or not.
add_filter('rest_prepare_product', 'prepareProductData', 10, 3);
function prepareProductData($response, $post, $request)
{
$params = $request->get_params();
$product_id_from_url = $params["id"];
$should_append_extra_data = !empty( $product_id_from_url);
//append extra data only when there's an id provided.
if($should_append_extra_data == true) {
//append here
}
}
So in this case the $product_id_from_url will have 123456 for /wp-json/wc/v3/products/123456
Request params can be used to get many other request parameters to modify responses for different situations.
I am using a function I created that I have tried creating customers from, and creating charges from. For whatever reason it seems to be double charging in test mode (Not bringing into live mode under these conditions) and I'm trying to understand why. I had it going through a few functions so I made it all happen in one function to make sure that it had nothing to do with what I had made. I'm lost on why this is happening. I try to make charges from token, doubles in less than a second. I try to create a customer from token, doubles in less than a second. I am using Stripes latest stripe-php library.
public function invoice($invoice = null) {
//Provides billing info for invoice.ctp
$this->loadModel('Invoice');
$billingi = $this->Invoice->get($invoice, [
'contain' => ['Items'],
]);
$dollars = 0;
foreach ($billingi->items as $item) {
$dollars += $item->price;
}
$cents = bcmul($dollars, 100);
$price = floatval($cents);
if ($this->request->is('post')) {
$stripeToken = $this->request->data('stripeToken');
//Sets stripe API
\Stripe\Stripe::setApiKey("sk_test_QVYouMViTf1k3zfVu2VAyZge");
//Retrieves stripe token from stripe API
//$response = \Stripe\Token::retrieve($stripeToken);
\Stripe\Customer::create(array(
"description" => "Test customer",
"source" => $stripeToken // obtained with Stripe.js
));
$this->Flash->success(__('Thank you for your payment!'));
return $this->redirect(['action' => 'approved', $invoice]);
}
/*
if ($response && $this->checkExists($response->card->cvc_check, $response->card->address_zip_check) == true) {
$this->insertCharge($invoice, $response, $price);
} else {
//Throw error because cvc_check or zip came back null (doesn't exist)
}
}
*/
$this->set('billingi', $billingi);
$this->set('_serialize', ['billing']);
}
The reason why there are things commented out is because I wanted to test the function without it, but adding it back later when I understand what the issue is.
In your code, the only API request sent to Stripe is a customer creation request (\Stripe\Customer::create(...)).
This doesn't charge the user -- it merely validates the card from the token in the source parameter, and creates a persistent customer object that you can in turn use to create actual charges. This tutorial explains this flow.
There's nothing in your code that would cause the API request to be sent twice. It's very unlikely the issue is on Stripe's end. More likely, your code is being called twice for some reason that's not related to Stripe. You'd need to add traces to your code to figure out what exactly is being called in what order.
For the Braintree_PaymentMethod::create() function, one of the options is:
'failOnDuplicatePaymentMethod', bool
If this option is passed and the payment method has already been added to the Vault, the request will fail. This option will not work with PayPal payment methods.
This appears to be a global compare. i.e. if the credit card information exists in the vault regardless of customer id this will fail.
Is there a way to check for duplicates on a particular customer?
Full disclosure: I work at Braintree. If you have any further questions, feel free to contact support.
You and Evan are correct: this is the only pre-built way of failing on duplicate creates regardless of customer create. You could achieve what you are trying to do with your own automation, however.
To do this, simply collect the credit card unique ids that already exist from the customer object. Then when you create the new payment method, compare it with the existing cards:
function extractUniqueId($creditCard){
return $creditCard->uniqueNumberIdentifier;
}
$customer = Braintree_Customer::find('your_customer');
$unique_ids = array_map(extractUniqueId,$customer->creditCards);
$result = Braintree_PaymentMethod::create(array(
'customerId' => 'your_customer',
'paymentMethodNonce' => 'fake-valid-discover-nonce',
));
if ($result->success) {
if(in_array(extractUniqueId($result->paymentMethod), $unique_ids)) {
echo "Do your duplicate logic";
} else {
echo "Continue with your unique logic";
}
}
Depending on what you want to do, you could delete the new payment method or whatever else you need.
Checked with Braintree support--still not available out of the box:
If you use failOnDuplicatePaymentMethod any request to add duplicate payment method information into the Vault will fail.
We currently don’t have the functionality to prevent a customer from adding a duplicate card to their profile, while allowing duplicate cards to still be added under multiple profiles. If this is something you are interested in you will have to build out your own logic.
#Raymond Berg, I made soem changes in your code, Here is the updated code:
1. Used foreach instead of in_array
2. Also delete the added card If found duplicate
$customer = Braintree_Customer::find('your_customer');
$unique_ids = array_map(extractUniqueId,$customer->creditCards);
$result = Braintree_PaymentMethod::create(array(
'customerId' => 'your_customer',
'paymentMethodNonce' => 'fake-valid-discover-nonce',
));
if ($result->success) {
$cardAlreadyExist = false;
$currentPaymentMethod = $this->extractUniqueId($result->paymentMethod);
//The in_array function was not working so I used foreach to check if card identifier exist or not
foreach ($unique_ids as $key => $uid) {
if( $currentPaymentMethod == $uid->uniqueNumberIdentifier)
{
$cardAlreadyExist = true;
//Here you have to delete the currently added card
$payment_token = $result->paymentMethod->token;
Braintree_PaymentMethod::delete($payment_token);
}
}
if($cardAlreadyExist) {
echo "Do your duplicate logic";
} else {
echo "Continue with your unique logic";
}
}
Here is a .NET version. Not 100% complete, but a good starter for someone with the same situation. If you find any issues or suggestions please just edit this answer.
try
{
// final token value (unique across merchant account)
string token;
// PaymentCreate request
var request = new PaymentMethodRequest
{
CustomerId = braintreeID,
PaymentMethodNonce = nonce,
Options = new PaymentMethodOptionsRequest()
};
// try to create the payment without allowing duplicates
request.Options.FailOnDuplicatePaymentMethod = true;
var result = await gateway.PaymentMethod.CreateAsync(request);
// handle duplicate credit card (assume CC type in this block)
if (result.Errors.DeepAll().Any(x => x.Code == ValidationErrorCode.CREDIT_CARD_DUPLICATE_CARD_EXISTS))
{
// duplicate card - so try again (could be in another vault - ffs)
// get all customer's existing payment methods (BEFORE adding new one)
// don't waste time doing this unless we know we have a dupe
var vault = await gateway.Customer.FindAsync(braintreeID);
// fortunately we can use the same nonce if it fails
request.Options.FailOnDuplicatePaymentMethod = false;
result = await gateway.PaymentMethod.CreateAsync(request);
var newCard = (result.Target as CreditCard);
// consider a card a duplicate if the expiration date is the same + unique identifier is the same
// add on billing address fields here too if needed
var existing = vault.CreditCards.Where(x => x.UniqueNumberIdentifier == newCard.UniqueNumberIdentifier).ToArray();
var existingWithSameExpiration = existing.Where(x => x.ExpirationDate == newCard.ExpirationDate);
if (existingWithSameExpiration.Count() > 1)
{
throw new Exception("Something went wrong! Need to decide how to handle this!");
}
else
{
// delete the NEW card
await gateway.PaymentMethod.DeleteAsync(newCard.Token);
// use token from existing card
token = existingWithSameExpiration.Single().Token;
}
}
else
{
// use token (could be any payment method)
token = result.Target.Token;
}
// added successfully, and we know it's unique
return token;
}
catch (BraintreeException ex)
{
throw;
}
catch (Exception ex)
{
throw;
}
Available for cards now as stated here . Not applicable for Paypal , Gpay and other payment methods. But this requires us to send the braintree customerID along.
I have a site that is using Stripe to process a subscription payments. There is only one type of subscription.
I followed this tutorial on NetTuts to do the initial setup.
Had a form working fine processing subscriptions and everything worked. Client requested a coupon code. Stripe supports this so I set out trying to add a coupon code to the existing form.
I set up coupon codes in Stripe, set my testing keys and switched to test mode in stripe.
I'm performing a couple of checks in my code:
Check to see whether a coupon was entered, if not create a new customer object without a coupon option
Check to see whether the Coupon is valid, if not return an error
If there has been a coupon entered and it is valid, then pass the matching Stripe coupon object as an option when creating a new customer.
if(isset($couponCode) && strlen($couponCode) > 0) {
$using_discount = true;
try {
$coupon = Stripe_Coupon::retrieve($couponCode);
if($coupon !== NULL) {
$cCode = $coupon;
}
// if we got here, the coupon is valid
} catch (Exception $e) {
// an exception was caught, so the code is invalid
$message = $e->getMessage();
returnErrorWithMessage($message);
}
}
try
{
if($using_discount == true) {
$customer = Stripe_Customer::create(array(
"card" => $token,
"plan" => "basic_plan",
"email" => $email,
"coupon" => $cCode
));
}
else {
$customer = Stripe_Customer::create(array(
"card" => $token,
"plan" => "basic_plan",
"email" => $email
));
}
$couponCode is populated with the form field correctly the same way all other fields are populated, I've triple checked that it is being pulled correctly.
When I try to submit the form without a coupon code, it charges the full amount and passes through Stripe correctly.
However if I enter either a valid OR invalid coupon code, it does not pass a coupon object with the customer object when creating a new customer object and charges the full amount when passing through Stripe.
I've looked at the code for hours and can't seem to figure out why it is always failing to recognize the discount code and pass the matching coupon object to Stripe.
This is probably a little out dated and you found a solution for your code. But it would seem all you need to do is pass through your original $couponCode as the array value for Coupon. As stated by codasaurus you are just getting an array back from stripe of the coupon, which you don't need unless you did $cCode->id and pass the ID back to your array to create a customer.
I changed when you set the $using_discount to true, as this would trigger to send a coupon code if the coupon was valid or not.
Once the coupon is actually valid we then send the coupon. You only need the value of your submitted state so $coupon is the reference to the discount in there system. Or you could use the $coupon->id if you wanted to create it that way.
Here is my take on a solution based on your code, it could be better, but I hope it helps others looking for a solution like me.
if($coupon!='' && strlen($coupon) > 0) {
try {
$coupon = Stripe_Coupon::retrieve($coupon); //check coupon exists
if($coupon !== NULL) {
$using_discount = true; //set to true our coupon exists or take the coupon id if you wanted to.
}
// if we got here, the coupon is valid
} catch (Exception $e) {
// an exception was caught, so the code is invalid
$message = $e->getMessage();
returnErrorWithMessage($message);
}
}
if($using_discount==true)
{
$customer = Stripe_Customer::create(array(
"card" => $token,
"plan" => $plan,
"email" => $id,
"coupon"=>$coupon) //original value input example: "75OFFMEMBER" stripe should be doing the rest.
);
}
else
{
$customer = Stripe_Customer::create(array(
"card" => $token,
"plan" => $plan,
"email" => $id)
);
}
Looking at the documentation https://stripe.com/docs/api#create_customer, the Stripe_Customer::create() call is looking for just the coupon code. It looks like you're passing in the entire coupon object.
Unless your first try catch fails, $couponCode already has your coupon code. Also, there are several other checks you need to perform to determine if the coupon is valid. For example, if the coupon->times_redeemed < coupon->max_redemptions, or if the coupon->redeem_by has passed, etc. You might also want to check if the customer is already using the coupon by checking the customer discount object.
If any of these checks fail, just set your $using_discount = false;
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.