Showing User Friendly Gateway Errors with braintree_php? - braintree

I've been using Braintree for about a year in an e-commerce website and I regularly hear from customers that they can't figure out why their payments are rejected. When a transaction is rejected, I display the error messages returned by Braintree (via braintree_php 5.x), which frequently looks like "Gateway Rejected: avs" (my customers do not know what any of those words mean).
Vexingly, these errors don't seem to be accompanied by an error code. Here is the (abbreviated) response object:
object(Braintree\Result\Error) {
success => false
[protected] _attributes => [
'errors' => object(Braintree\Error\ErrorCollection) {
[private] _errors => object(Braintree\Error\ValidationErrorCollection) {
[protected] _collection => []
[private] _errors => []
[private] _nested => []
}
},
[...]
'message' => 'Gateway Rejected: avs',
[...]
'creditCardVerification' => null,
'subscription' => null,
'merchantAccount' => null,
'verification' => null
]
}
So, in order to catch these gateway errors and communicate them intelligibly to my customers, I've had to rely on the inspection of $result->message. My code looks something like this:
$result = $this->gateway->transaction()->sale([...]);
if (!$result->success) {
switch ($result->message) {
case 'Gateway Rejected: avs':
$error = 'The billing postal code you provided failed validation. Please try again.';
break;
case 'Gateway Rejected: avs_and_cvv':
$error = 'The postal code and/or CVV you provided failed validation. Please try again.';
break;
case 'Gateway Rejected: cvv':
$error = 'The CVV you provided failed validation. Please try again.';
break;
case 'Gateway Rejected: duplicate':
$error = 'Payment rejected as duplicate. If you feel this is in error, please contact us.';
break;
case 'Gateway Rejected: fraud':
$error = 'Payment rejected. Please try another payment method.';
break;
case 'Gateway Rejected: risk_threshold':
$error = 'Payment rejected. Please try another payment method.';
break;
default:
$error = $result->message ?? 'Payment failed. Please check your payment information and try again.';
break;
}
}
This is working, but it feels like the wrong approach. For instance, what if Braintree modifies the $result->message text? And why am I not being given an error code? I would assume that this is a problem almost everyone who uses braintree_php would encounter, but I haven't been able to find any mention of it or any code demonstrating an alternative method to handle errors.
I'd greatly appreciate any feedback on my work around and alternatives to it! (And on the chance that this is actually the best way to handle these errors, I'm documenting my approach here for others.)

You should be getting an error code back. Trying to catch messages like you point out is a bad idea for the reason you(and they) mention about them changing the text.
The error codes are here - https://developer.paypal.com/braintree/docs/reference/general/validation-errors/all/php#transaction
This is what I am doing to catch the codes and display the message. I am returning an array of errors, but you can do whatever works for you.
$error_array = [];
if ($result->success) {
//Process Transaction
} elseif (!is_null($result->transaction)){
$error_array[] = "Transaction status - " . $result->transaction->status;
} else {
foreach($result->errors->deepAll() as $error) {
switch($error->code){
case 91569:
case 81725:
$error_array[] = 'Payment method is not valid';
break;
default:
$error_array[] = $error->message;
}
}
}
Some of their message text is not user friendly, so you will want use the switch statement to catch the code and give a clearer message.

Related

Stripe PaymentIntent with confirmation method manual fails every time

I'm using Laravel with a personal integration of the Stripe API (using Stripe API from github).
Everything was working fine until i switched to manual confirmation mode, and now i'm receiving the following error:
This PaymentIntent pi_**************uVme 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`.
Any idea?
This is my current code (which is not working):
Stripe::setApiKey(config('services.stripe.secret')); // config('services.stripe.secret') returns "sk_test_gFi********************nMepv"
$paymentIntent = PaymentIntent::create([
'amount' => $orderSession->order_total * 100,
'currency' => 'eur',
'description' => "Pagamento di ".(price($orderSession->order_total))."€ a ".$orderSession->user->user_name." in data ".(now()->format("d-m-Y H:m:s")),
'metadata' => [
'subtotal' => $orderSession->order_subtotal,
'user'=> "{$orderSession->user_id} : {$orderSession->user->user_email}",
'wines'=> substr(
$orderSession->wines()->select('wine_id', 'quantity')->get()->each(
function($el){
$el->q= $el->quantity;
$el->id = $el->wine_id;
unset($el->wine_id, $el->pivot, $el->quantity);
}
)->toJson(),
0,
500
),
],
'confirmation_method' => 'manual',
]);
JS frontend:
<button class="myButtonPayment" id="card-button" type="button" data-secret="{!!$stripePaymentIntent->client_secret!!}" ><span>Pay</span></button>
...
<script>
cardButton.addEventListener('click', function() {
if(!document.getElementById('order_telephone_number').value || /^\+?[0-9 ]{6,20}$/.test(document.getElementById('order_telephone_number').value)){
stripe.handleCardPayment(
clientSecret, cardElement, {
payment_method_data: {
billing_details: {name: cardholderName.value}
}
}
).then(function (result) {
if (result.error) {
console.log(result.error);
} else {
document.getElementById('myForm').submit();
}
});
}
});
</script>
The error is occuring when I click on the button (so is not related to the part of the code where I confirm the payment)
The error serialization is the following:
{
"type":"invalid_request_error",
"code":"payment_intent_invalid_parameter",
"doc_url":"https://stripe.com/docs/error-codes/payment-intent-invalid-parameter",
"message":"This PaymentIntent pi_1H3TQ*********T00uVme 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`.",
"payment_intent":{
"id":"pi_1H3***********uVme",
"object":"payment_intent",
"amount":2060,
"canceled_at":null,
"cancellation_reason":null,
"capture_method":"automatic",
"client_secret":"pi_1H3TQ********T00uVme_secret_2T7Di*********nkoaceKx",
"confirmation_method":"manual",
"created":1594415166,
"currency":"eur",
"description":"....",
"last_payment_error":null,
"livemode":false,
"next_action":null,
"payment_method":null,
"payment_method_types":[
"card"
],
"receipt_email":null,
"setup_future_usage":null,
"shipping":null,
"source":null,
"status":"requires_payment_method"
}
}
Manual confirmation for Payment Intents is for server-side confirmation only (i.e. with your secret API key, not your publishable key). Setting confirmation_method to manual on a Payment Intent is the same as saying, "this Payment Intent can only be confirmed server-side".
You can read more about this in in the finalize payments on the server guide in Stripe's documentation.

Stripe PHP Best way to debug an SignatureVerificationException with Stripe-cli

I created a webhook to get informations about checkout sessions :
public function stripeWebhookCheckout(Request $request)
{
\Stripe\Stripe::setApiKey(env("STRIPE_SECRET"));
// You can find your endpoint's secret in your webhook settings
$endpoint_secret = 'whsec_fVBkAmCztUTacQKZiyjmcq6QQrl8lKL1';
$payload = #file_get_contents('php://input');
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$event = null;
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$sig_header,
$endpoint_secret
);
} catch (\UnexpectedValueException $e) {
// Invalid payload
http_response_code(400);
exit();
} catch (\Stripe\Exception\SignatureVerificationException $e) {
// Invalid signature
http_response_code(400);
exit();
}
// Handle the checkout.session.completed event
if ($event->type == 'checkout.session.completed') {
$session = $event->data->object;
// Fulfill the purchase...
handle_checkout_session($session);
$stripeSessionId = $session['id'];
if (isset($stripeSessionId)) {
$payment = Payment::where('stripe_sessioncheckout_id', '=', $stripeSessionId)->first();
$payment->status = "success";
$payment->data = $session;
$payment->save();
}
}
http_response_code(200);
}
I use stripe-cli to test my webhook in local. And i have this kind of result
$ stripe listen --forward-to jvlb.test/api/stripe/webhook/checkout
> Ready! Your webhook signing secret is whsec_sc9Gh9A6zx3IOfBpH62F9DdPYIhSUYtw (^C to quit)
2019-11-28 16:19:06 --> charge.succeeded [evt_1FjsW5FIYszmshR0eGSB9GDo]
2019-11-28 16:19:06 <-- [400] GET https://jvlb.test/api/stripe/webhook/checkout [evt_1FjsW5FIYszmshR0eGSB9GDo]
To debug it i changes the http_response_code(400) and I realised it generate a SignatureVerificationException.
My question is, how can i debug this ? Is it the $_SERVER['HTTP_STRIPE_SIGNATURE'] who is wrong ?
Thanks
i found a solution, if it can help people in the future :
I made a mistake in the way i use stripe-cli, i forgot "https://".
The good way is :
stripe listen --forward-to https://jvlb.test/api/stripe/webhook/checkout
And then i had few error of code to manage. I just used the tail command on my log file
tail -f storage/logs/laravel-2019-11-29.log

Laravel issue Credentials are required to create a Client

I need to test send SMS to mobile I get Credentials are required to create a Client error for My Code Here
.env
TWILIO_ACCOUNT_SID=AC15...................
TWILIO_AUTH_TOKEN=c3...................
TWILIO_NUMBER=+1111...
Config\App
'twilio' => [
'TWILIO_AUTH_TOKEN' => env('TWILIO_AUTH_TOKEN'),
'TWILIO_ACCOUNT_SID' => env('TWILIO_ACCOUNT_SID'),
'TWILIO_NUMBER' => env('TWILIO_NUMBER')
],
Controller
$accountSid = env('TWILIO_ACCOUNT_SID');
$authToken = env('TWILIO_AUTH_TOKEN');
$twilioNumber = env('TWILIO_NUMBER');
$client = new Client($accountSid, $authToken);
try {
$client->messages->create(
'0020109.....',
[
"body" => 'test',
"from" => $twilioNumber
// On US phone numbers, you could send an image as well!
// 'mediaUrl' => $imageUrl
]
);
Log::info('Message sent to ' . $twilioNumber);
} catch (TwilioException $e) {
Log::error(
'Could not send SMS notification.' .
' Twilio replied with: ' . $e
);
}
Twilio developer evangelist here.
A quick read over the environment config for Laravel suggests to me that you can use the env method within your config files, as you are doing, but it's not necessarily available in application code. Since you are committing your environment variables to the config object, I think you need to use the config method instead.
$accountSid = config('TWILIO_ACCOUNT_SID');
$authToken = config('TWILIO_AUTH_TOKEN');
$twilioNumber = config('TWILIO_NUMBER');
Let me know if that helps at all.

New Caller Insert to Database with Codeigniter using the Twilio API

Below is the function to receive all incoming calls in my Controller
public function call_incoming()
{
$blocklist = $this->call_log_model->get_blocklist($_REQUEST['From']);
$tenantNum = $this->call_log_model->get_called_tenant($_REQUEST['From']);
$tenantInfoByNumber = $this->account_model->getTenantInfoByNumber($tenantNum->to_tenant);
$officeStatus = $this->check_office_hours($tenantInfoByNumber->start_office_hours, $tenantInfoByNumber->end_office_hours);
$calldisposition = $this->calldisp_model->get_call_disposition($tenantInfoByNumber->user_id);
$response = new Services_Twilio_Twiml;
if($blocklist == 0)
{
if($officeStatus == "open")
{
if($_POST['Called'] != AGENTPOOL_NUM)
{
$data = array(
'caller'=>$_REQUEST['From'],
'to_tenant'=>$_POST['Called'],
'date_created'=>date('Y-m-d H:i:s')
);
$this->call_log_model->insert_caller_to_tenant($data);
$dial = $response->dial(NULL, array('callerId' => $_REQUEST['From']));
$dial->number(AGENTPOOL_NUM);
print $response;
}
else
{
$gather = $response->gather(array('numDigits' => 1, 'action'=>HTTP_BASE_URL.'agent/call_controls/call_incoming_pressed', 'timeout'=>'5' , 'method'=>'POST'));
$ctr = 1;
foreach($calldisposition as $val )
{
$gather->say('To go to '.$val->disposition_name.', press '.$ctr, array('voice' => 'alice'));
$gather->pause("");
$ctr++;
}
print $response;
}
}
else
{
$response->say('Thank you for calling. Please be advise that our office hours is from '.$tenantInfoByNumber->start_office_hours.' to '.$tenantInfoByNumber->end_office_hours);
$response->hangup();
print $response;
}
}
else
{
$response->say('This number is blocked. Goodbye!');
$response->hangup();
print $response;
}
}
Please advise if I need to post the model...
Here is whats happening everytime an unknown number calls in, the caller will hear an application error has occurred error message and when checking the Twilio console the error it is giving me is
A PHP Error was encountered
Severity: Notice
Message: Trying to get property of non-object
Filename: agent/Call_controls.php
Line Number: 357
Please be advised that this error only occurs when the caller is a number not in our database yet. When the call comes from a number already saved in our databse, this codes works...
Thank you for the help...
if($tenantNum) {
$tenantInfoByNumber = $this->account_model->getTenantInfoByNumber($tenantNum->to_t‌​enant);
} else {
$tenantInfoByNumber = ""; // fill this in with relevant fill data
}
This should fix your issue, as there is no TenantNum returned, there is no data, so make it yourself for unknown numbers.

Laravel Validator / Uploaded File Fails At Required

"dd" output of Input::all() in postController:
array(8) {
["_token"]=>
string(40) "6WZ87M1LCiVCsaUS9HbjZckRibXfF2RP69LCpW7K",
...
...
["svg"]=>
object(Symfony\Component\HttpFoundation\File\UploadedFile)#9 (7) {
["test":"Symfony\Component\HttpFoundation\File\UploadedFile":private]=>
bool(false)
["originalName":"Symfony\Component\HttpFoundation\File\UploadedFile":private]=>
string(39) "Screenshot from 2013-06-18 17:07:27.png"
["mimeType":"Symfony\Component\HttpFoundation\File\UploadedFile":private]=>
string(9) "image/png"
["size":"Symfony\Component\HttpFoundation\File\UploadedFile":private]=>
int(29747)
["error":"Symfony\Component\HttpFoundation\File\UploadedFile":private]=>
int(0)
["pathName":"SplFileInfo":private]=>
string(14) "/tmp/phpdRTDU7"
["fileName":"SplFileInfo":private]=>
string(9) "phpdRTDU7"
}
}
Validation:
$rules = array('svg' => 'required');
$check = Validator::make(Input::except('_token'), $rules);
if($check->fails()){
return Redirect::back()->withErrors($check);
}else{
return Redirect::back()->with('message', 'No problem');
}
And I get the error message:
Error message:
The svg field is required.
Even if I upload file as you see on dd output, it shows that error always.
Thanks,
user2413500 found that the problem was using Input::except('_token') which did not include the file object. However, Input::all() does include the file object. This seems to be a bug which I'll report, but the definition of Input::except is "all" minus the items you don't want.
But what seems to be happening is "all" minus the items you don't want, minus your file!
Itrulia and Taylor say this is not a bug.
However, these are confusingly not identical statements when you have a $_FILE posted...
$params = Input::except('_token'); // Missing file inputs!
$params = array_except(Input::all(), '_token'); // The current solution.
Be on guard! :)

Resources