I'm getting an Authorization Failed response from 2Checkout when using OmniPay API.
I had successfully hooked up Stripe with OmniPay, so then I went about hooking up 2Checkout. When you go to do that, the initial package on Github warns you to get the newer version from a user collizo4sky. So, I did, and then got it to work slightly without fatal error.
In my web form, I generate a token properly and the sandbox logs at 2Checkout confirm this. I then use this code in OmniPay to attempt to charge the card:
use Omnipay\Omnipay;
$sMerchantTransID = rand(11111111,99999999);
$oGateway = Omnipay::create('TwoCheckoutPlus_Token');
$oGateway->setPrivateKey($config->TWOCHECKOUT_PRIVATE_KEY);
$oGateway->setAccountNumber($config->TWOCHECKOUT_SELLERID);
$oGateway->setTestMode(true);
$oResponse = $oGateway->purchase(array(
'amount' => $sPrice,
'currency' => 'USD',
'token' => $sToken,
'transactionId' => $sMerchantTransID
))->send();
if (!$oResponse->isSuccessful()) {
die('ERROR: ' . $oResponse->getMessage());
}
However, the message comes back with "Authorization Failed". So, I went into the source code of the OmniPay API for collizo4sky's package in this path...
omnipay/vendor/collizo4sky/omnipay-2checkout/src/Message/TokenPurchaseRequest.php
...and added some log file debugging of the sendData() class method. This is what it responded with, and note that I changed some of the values for obvious privacy reasons:
array (
'sellerId' => '901414261',
'privateKey' => 'EAEC8615-4C48-4D98-B7E5-4B6D8865E1BA',
'merchantOrderId' => 65639323,
'token' => 'FDI1ZTM3N2UtY2VkZS00NTM1LWE5MTctYzI4MjA5YWI4Yjhm',
'currency' => 'USD',
'total' => '519.00',
)
Yes, the sellerID and the privateKey came from the sandbox account, as did the publishable key when I generated the token in the web form in the first place.
So, anyway, when this is submitted to the sandbox URL, it returns with this response:
HTTP/1.1 400 Bad Request
Server: Apache-Coyote/1.1
Cache-Control: no-cache, no-store, must-revalidate
Date: Fri, 22 Apr 2016 03:30:37 GMT
Expires: 0
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
{"validationErrors":null,"response":null,"exception":{"errorMsg":"Authorization Failed","httpStatus":"400","exception":false,"errorCode":"600"}}
What am I doing wrong?
EDIT:
I then tried using the 2Checkout-supported PHP library that they provide. I did the exact same transaction and it too returns "Authorization Failed".
I'm wondering if it's a setting on my account even though I'm using a sandbox? I mean, I have not yet been approved in production, and was trying to get the sandbox going until then so that I could do a demo.
I also regenerated API keys in the sandbox and tried again, and that failed.
The credit card I used was the one they provide on the sandbox under the sandbox API keys, and I used a CVV of 123 with 12/18 as the expiration date. I then thought maybe it wanted a 4 digit year, so I updated that and tried again, and that failed too with the same consistent error of "Authorization Failed".
When I check on what "Authorization Failed" means, it appears that it's the credit card that's the problem.
My gut feeling so far is telling me that I'm doing everything correct and that it's a flag on my account that's the issue. I mean, if my regular account is not yet approved, will the sandbox still work?
The only way that I could get a transaction to go through was to set the Demo mode to "Off" manually in the sandbox settings under Account > Site Management, and then to pass in at least a billing address. I'd love to find out how to not have the billing address requirement (such as for digital download goods), but have not found a way to do so just yet, or if that's even possible.
Please note that if you set Demo Mode to "On", plus don't send a billing address, the transaction will go through okay but then it won't appear as a sale under the Sales tab. That's not very useful, because when you go live you want an actual sale that you can refund, and Demo Mode doesn't simulate an actual charge, merely an authorization of that card. (At least that's what I've figured out.)
On the transactionId parameter, I took a shortcut for purposes here. Please make it a unique number in your system (like some number generated with UNIX time) or you could end up with a potential collision.
I also tried commenting out just the email, or just the phone, or both, and that didn't work -- you'll get "Parameter Error". Then, I tried sending in only these fields under card: billingName, email, billingPostcode, and billingCountry -- and that failed with "Parameter Error" too. So then I added billingState -- "Parameter Error". Then, added billingCity -- "Parameter Error". So, in order for the transaction to go through, unless someone can show me some setting override or technique, the Billing Address is required, and must also include email and phone, to my wild surprise. That's a showstopper for some people, I'm sure, such as those trying to sell non-tangibles like digital downloads. Note also that I tried the official 2Checkout-created PHP API and tried all these tests again with that, and again, received "Parameter Error" unless the full billing address (including email and phone too) was utilized. This full billing address requirement with also email and billing phone is going to be a bummer for some businesses, I'm sure.
EDIT: This answer, if I'm guessing right, is an official answer from someone working at 2Checkout, itself? It sure does look that way. Anyway, he says that the billing address is definitely required because, as he states, "This is a requirement of our banking partners for address verification."
EDIT2: You'll need to experiment with a real live transaction, but I discovered in at least sandbox mode that, if I pass an empty string for the phoneNumber field on the official 2Checkout PHP library, or the billingPhone in the OmniPay API, then the transaction goes through okay. They just want to see that parameter, although it can be empty. But don't take my word for it -- test it on a live transaction (and refund yourself) to reconfirm, as I was only doing this in the sandbox. This answer seems to officially confirm from 2Checkout itself that they allow this.
Fixed code:
use Omnipay\Omnipay;
$sMerchantTransID = rand(11111111,99999999);
$oGateway = Omnipay::create('TwoCheckoutPlus_Token');
$oGateway->setPrivateKey($config->TWOCHECKOUT_PRIVATE_KEY);
$oGateway->setAccountNumber($config->TWOCHECKOUT_SELLERID);
$oGateway->setTestMode(true); // turns on Sandbox access
$oResponse = $oGateway->purchase(array(
'amount' => $sPrice,
'currency' => 'USD',
'token' => $sToken,
'transactionId' => $sMerchantTransID,
'card' => array(
'billingName' => $sName,
'billingAddress1' => $sStreet1,
'billingAddress2' => $sStreet2,
'billingCity' => $sCity,
'billingState' => $sState,
'billingPostcode' => $sZip,
'billingCountry' => $sCountry,
'email' => $sEmail,
'billingPhone' => $sPhone
)
))->send();
if (!$oResponse->isSuccessful()) {
die('ERROR: ' . $oResponse->getMessage());
}
Related
I'm using Stripe's PHP SDK in a Laravel project, and I'm having a weird issue with 3d secure payments.
PHP Version : 7.4
Stripe-PHP : 7.75.0
When creating a PaymentIntent on behlaf of a connected stripe account using the stripe_account parameter, and the confirmation_method: 'manual' parameter, there's alway an error stating:
This PaymentIntent pi_XXXXXX cannot be confirmed using your publishable key because its confirmation_method is set to manual. Please use your secret key instead, or create a PaymentIntent with confirmation_method set to automatic.
This is how I created my intent:
$paymentIntentParameters = array(
'amount' => $priceAsCents,
'currency' => 'eur',
'payment_method' => $paymentMethodId,
'confirmation_method' => 'manual',
'confirm' => true,
);
$paymentIntent = StripePaymentIntent::create(
$paymentIntentParameters,
['stripe_account' => $store->stripe_token]
);
I followed everything said here: https://github.com/stripe-samples/accept-a-card-payment/tree/master/without-webhooks
I'm in a case where the webhook doesn't work for me, and where the confirmation_method: 'automatic' doesn't do the job too, because the confirmation is done on the frontend, and we only want cofirmations on the backend.
Is there any quick fix for this?
In order to perform server-side (manual) confirmation, you want to follow this doc. Specifically, instead of using handleCardPayment on the frontend, you use createPaymentMethod on the frontend, use the resulting PaymentMethod with a PaymentIntent on the backend, and then confirm on the backend. The linked doc shows the exact steps.
And for the 3d secure, you need to add handleCardPayment.
I'm using the theiconic/php-ga-measurement-protocol package and have followed the exact steps as described in the readme, but for reasons I don't quite understand, half of the info does not show up in Google Analytics.
I'm using the following code:
use TheIconic\Tracking\GoogleAnalytics\Analytics;
$trackingID = 'xxxxxxx';
$order = 'obviously an object';
$deal = 'object';
$analytics = new Analytics();
// the Client ID just won't do anything
$analytics->setProtocolVersion('1')
->setClientId($order->gaClientID)
->setTrackingId($trackingID);
// this part works just fine
$analytics->setTransactionId($transactionID)
->setRevenue($order->getTotalPrice(false))
->setTax($order->getTaxCost())
->sendTransaction();
// here it's as if nothing happens
// Yes, it does loop over all the orderRules but it does not show up in Google Analytics
foreach ($order->getOrderRules() as $orderRule) {
$analytics->setTransactionId($transactionID)
->setItemName($deal->name)
->setItemCode($order->dealID)
->setItemCategory($deal->getDealCategoryName())
->setItemPrice($orderRule->getPrice())
->sendItem();
}
And I'm saving the ClientID with this JavaScript into a hidden input:
ga.getAll()[0].get('clientId')
So my issues are basically:
client ID does not work
Items do not appear in Google Analytics Ecommerce
Is there anything I am forgetting? I've had several people looking at it, not being able to find out where it's going wrong.
What I would do:
Enhanced e-commerce: replace your code which is for the old e-commerce with this code which is for enhanced e-commerce. Reason: new (enhanced) is backward compatible with the old and has lots more features, so 0 point of using old e-commerce. Reason 2: the enhanced e-commerce implementation seems "cleaner" in the sense that all the data is sent with 1 single hit at the end (->sendEvent();), whereas the old e-commerce first sends the transaction (->sendTransaction();) then later the products with separate hits (->sendItem();).
Debug: enable debug mode to find out if/why your hits are being rejected by the API.
With enhanced e-commerce it should be something like:
$analytics->setDebug(true)
->setEventCategory('Checkout')
->setEventAction('Purchase')
->sendEvent();
$debugResponse = $response->getDebugResponse();
print_r($debugResponse);
And you should get debug from the API that will look like this:
{
"hitParsingResult": [
{
"valid": false,
"hit": "GET /debug/collect?tid=fake\u0026v=1 HTTP/1.1",
"parserMessage": [
{
"messageType": "ERROR",
"description": "The value provided for parameter 'tid' is invalid. Please see ... for details.",
"parameter": "tid"
},
Filters: if your hits are not rejected by the API (once they are validated you have to remove the debug for them to be actually sent), I would look in GA to check if you don't have filters excluding your hits.
Quotas: although unlikely, I would also check if you haven't reached the API limits hence why your data isn't being collected.
BONUS: User-ID: if people making purchases have created a user account on your website, use your database ID as User ID: it will be easier to implement (you don't have to reverse engineer the Client ID, use your database ID), and will track the same user regardless of the browser/device used (Client ID is linked to the Cookie, so will be different for each device/browser the user uses, your database ID will always be the same as long as people log in with the same account)
I've tried the enhanced e-commerce again. I've done that before as well, but this time with the debug function on. The client ID gets send properly as seen in hitPersingResult[0]['hit']. But still nothing appears in google Analytics.
Also we're not hitting the limits. I've literally copy pasted the example and this is my response:
Array
(
[hitParsingResult] => Array
(
[0] => Array
(
[valid] => 1
[parserMessage] => Array
(
)
[hit] => /debug/collect?v=1&tid=UA-xxxxxxxx-11&cid=161460xxxx.xxxx180000&uid=161460xxxx.xxxx180000&ti=1802.48511-1518713633&ta=test%20affiliation&tr=206.95&tt=3.4623966942149&ts=0&pa=purchase&ec=Checkout&ea=Purchase&t=event&pr1id=2033&pr1nm=xxxxx&pr1br=brand&pr1ca=xxxxxx&pr1pr=xxx&pr1qt=1
)
)
[parserMessage] => Array
(
[0] => Array
(
[messageType] => INFO
[description] => Found 1 hit in the request.
)
)
)
For my shopping site I have a fairy simple stripe implementation on the frontend where user hits "pay through stripe" button fills in CC details in a popup and hits proceed. If all goes well I get a token as $_POST['stripeToken'] which I process like below (for web payments)
try
{
$stripe = new Stripe();
$stripe = Stripe::make();
$customer = $stripe->customers()->create([
'email' => $_POST['email_id'],
'source' => $_POST['stripeToken']
]);
$charge = $stripe->charges()->create([
'customer' => $customer['id'],
'amount' => $total_amount, // note this is calculated again on server to prevent fraud
'currency' => 'sgd'
]);
}
catch (\Cartalyst\Stripe\Exception\CardErrorException $e)
{
return view('front.payment_error')->with('message', $e->getMessage());
}
Thing is we cannot rely on $total_amount coming from frontend form submission as user can easily spoof this amount and pay just $1 in stripe and get a token and spoof $total_amount to 1 thus getting products at whatever price he wants ,that's why I need to calculate his `$total_amount again on the server (from his cart) and use that to process stripe on the server.If that amount doesn't match what the token stands for , stripe would automatically raise an exception and prevent fraud.
So far good.. but the problem comes when dealing with API , mobile app will process stripe using their own libs. on the client side they would just send the final (for recording in db) but obviously I cannot use it to charge since that is already done on the mobile.Since these days it's very easy to change app behaviour (by patching apks) or craft custom HTTP request (postman) , server check is a must in case of payments.
So my question is how can I verify from the token the actual amount user paid
ie. reverse convert stripeToken => actual paid amount
Update:
This is what I am looking in case of Stripe
https://developer.paypal.com/docs/integration/mobile/verify-mobile-payment/
Quetion:
How would I cancel a pre-approved payment using PayPal's API operations? I have the Pre-approval key of the payment to be cancelled but I can't find a way to do it.
I did some digging over the internet and found this CancelPreapproval API Operation but that document is not very helping for a starter like me. Few things are missing like what would be the link where I'd send the cancel request? Can't find any example. Need help.
I did tried cancelling the payment by using this code but it fails.
$security_user_id = $neworder->security_user_id;
$security_password = $neworder->security_password;
$security_signature = $neworder->security_signature;
$security_application_id = $neworder->security_application_id;
$headers_array = array("X-PAYPAL-SECURITY-USERID" => $security_user_id,
"X-PAYPAL-SECURITY-PASSWORD" => $security_password,
"X-PAYPAL-SECURITY-SIGNATURE" => $security_signature,
"X-PAYPAL-APPLICATION-ID" => $security_application_id,
"X-PAYPAL-REQUEST-DATA-FORMAT" => "NV",
"X-PAYPAL-RESPONSE-DATA-FORMAT" => "JSON",
);
$pay_result = wp_remote_request('https://svcs.sandbox.paypal.com/AdaptivePayments/Preapproval', array('method' => 'POST', 'timeout' => 20, 'headers' => $headers_array, 'body' => $maincode));
This PayPal PHP SDK will make it very quick and easy for you. Just extract it into your project structure as you would any other class, and make sure to setup the config file correctly with your own sandbox and/or live PayPal credentials.
Then you can look in the /templates and you'll see a CancelPreapproval.php setup and ready to go. It'll be fully functional. All you'll have to do is plug in your preapproval key and it'll handle the rest for you successfully.
There are lots of /samples and there is a file in /templates for pretty much every API call PayPal has.
You can get this done within minutes using that class. Let me know if you have any specific questions about it.
I am trying to integrate Pin.net.au CC processing into my site. I am using Omnipay library to make the calls.
To not store CC details in my server, I am using the Pin.js token method.
On form submit page (after user fills in personal and CC details) javascript does a 'prevent default' and sends the data from forms (browser) straight to pin.net.au servers. Server sends a card_token in response and resubmits the form to my server.
This token is recieved successfully and I can output it in my tests.
I get into trouble when I take that token and send a purchase request to pin.net.au. According to the API docs, I need not send user and card details when I send the token (the entire point of the token, really). I send this token along with other compulsory bits like email, amount, description etc.
This works when I cURL on my terminal and I get a charge success.
However, sending this purchase/charge request using the Omnipay library, each time I get a 422 (invalid resource) that asks for the user details and CC information. It should have populated this stuff from the token I sent.
I have scoured the API docs of both Omnipay and Pin.net.au. I don't seem to be doing anything wrong. What am I missing?
Here's my charge request:
$gateway = GatewayFactory::create('Pin');
$gateway->setSecretKey('MY_SECRET_KEY');
$response = $gateway->purchase([
'email' => 'user#email.com',
'description' => 'Package',
'amount' => '99',
'currency' => 'AUD',
'card_token' => Input::get('card_token'),
'ip_address' => Input::get('ip_address')
])->send();
Finally, it shouldn't really matter but if you'd like to know, I'm using Laravel 4.
Your example request has an amount of 99, the minimum amount for a Pin Payments charge is $1 (amount = 100).
I don't think this is the problem you are referring to though, it looks like Omnipay does not support using the card_token gear. If you go look over here - https://github.com/adrianmacneil/omnipay/blob/master/src/Omnipay/Pin/Message/PurchaseRequest.php#L34 - you can see Omnipay isn't sending the card_token field with it's request, it only tries to send card details, which obviously aren't present from your example!
Perhaps you could get in touch with the Omnipay developers or write a pull request yourself!
This is fixed in Omnipay v1.0.4 - you should be able to use the token like this:
$gateway = GatewayFactory::create('Pin');
$gateway->setSecretKey('MY_SECRET_KEY');
$response = $gateway->purchase([
'description' => 'Package',
'amount' => '99.00',
'currency' => 'AUD',
'token' => Input::get('token'),
'ip_address' => Input::get('ip_address'),
'card' => ['email' => 'user#email.com'],
])->send();