Some bookings have problem when customer paid using Payone wallet method (paypal) - php

I have implemented the Payone payment gateway (Creditcard, Sofort, Paydirect and Paypal) successfully. After successful payment I am getting txaction response (appointed and paid) and everything is fine. But sometimes I am not getting response from Payone after customer paid using Paypal(I checked around 60 successful transactions. But in that 2 or 3 transactions are not got response and the customer's amount has been deducted from their account).
After successful transaction payone is posting data in to this route
/* Response from payone */
Route::post('/payment/response', 'PaymentController#response')->name('payment.response');
I think laravel request is not capturing data from url. or
There is something wrong to using this method Schema::hasColumn.
Any help would be appreciated thanks.
PaymentController.php
public function response(Request $request)
{
// Here I created to store all request in to table but data is not storing.
/* Testing purpose begin */
$payment = new Payment;
foreach($_POST as $key => $value) {
if(Schema::hasColumn($payment->getTable(), $key)){
if(is_array($value)) {
$payment->{$key} = $value[1];
} else {
$payment->{$key} = $value;
}
}
}
$payment->save();
/* Testing purpose end */
if ($_POST["key"] == hash("md5", env('KEY'))) {
echo "TSOK"; // If key is valid, TSOK notification is for PAYONE
$user = Userlist::where('is_delete', 0)
->where('usrActive', '1')
->where('userid', $_POST["userid"])
->first();
if($user && $_POST["clearingtype"] && $_POST["txaction"]) {
$bookings = Booking::select('_id', 'old_booking_id', 'status', 'payment_status')
->where('user', new \MongoDB\BSON\ObjectID($user->_id))
->whereIn('status', ['5', '8', '10', '11']) //5=>Waiting for payment, 8=>Cart, 10=> Temporary (This status is using in edit booking section), 11=> On processing
->where('is_delete', 0)
->where('txid', $_POST["txid"])
->where('userid', $_POST["userid"])
->get();
if($bookings) {
if ($_POST["txaction"] == "appointed") {
update booking status and sent email
}
else if ($_POST["txaction"] == "paid") {
update paid status
}
else {
update failed status
}
}
}
}
}
laravel log
[2018-09-11 09:04:14] production.ERROR: Method [error] does not exist on [App\Http\Controllers\PaymentController]. {"userId":"5afa790212236cc4660ed509","exception":"[object] (BadMethodCallException(code: 0): Method [error] does not exist on [App\\Http\\Controllers\\PaymentController]. at /var/www/vhosts/cabin-holiday.frontend/vendor/laravel/framework/src/Illuminate/Routing/Controller.php:68)

Method [error] does not exist on [App\Http\Controllers\PaymentController].
Write error method on the PaymentController. Try to find documentation on error method mentioned somewhere in your payment gateway documentation. And read when it is called and why. So that you will get idea of what to handle in error method in PaymentController. You might have to declare route or may be it is already there in your routes since it is not complaining about route but method. Hope this helps you.

The solution for this issue is to change the method it should be get method. Because we are getting response from the paypal in url. So you need to set the GET method.
/* Response from payone */
Route::get('/payment/response', 'PaymentController#response')->name('payment.response');

Finally found the issue. Reason for this issue is sometimes response data like address, city etc are not getting in correct format (Users are from Germany). So I strictly converted the response data in to UTF-8 format and stored important data in to database.

Related

How do I do a callback function in Laravel after a transaction processes

What I'm trying to do here is to implement a callback function in a Laravel 5.4 controller. This uses Authorize.net to process a credit card transaction, then inserts a bunch of stuff into the database, sends some messages, makes an invoice and airbill, and so on.
What I WANT to happen is:
Hit the "Submit" button, sends AJAX request
Processes the Authorize.net transaction
If good, then call a callback function to do all the gruntwork but return a transaction response.
4) Notify the user
The reason I wanna do it this way is that I want the user to wait the minimum amount of time to see the result of their payment processing without having to wait another 5 seconds or so staring at a spinning wheel waiting to go to the order complete page.
Can a callback function help me do this?
Thanks
My current implementation results in a 500 error, and I'm not quite sure what I should do from here...
[ route in web.config ]
// AJAX call to process the transaction, insert the new order, inform the user of success/failure
Route::post('/shop/processtransaction', 'OrderCheckoutController#processTransaction');
[ function processTransaction in OrderCheckoutController.php ]
public function processTransaction(Request $request) {
return self::processPaymentAndOrderInsertion($request, 'createOrder');
}
[ function processPaymentAndOrderInsertion in OrderCheckoutController.php ]
public function processPaymentAndOrderInsertion(Request $request, callable $createOrderCallback = null) {
$order_proc = new OrderProcessingTools;
$transaction_response = $order_proc->processTransaction($request);
if($transaction_response['success'] === true) {
self::$createOrderCallback($request, $transaction_response);
}
return json_encode($transaction_response);
}
[ my callback function ]
public function createOrder(Request $request, $transaction_response) {
$order_proc = new OrderProcessingTools;
$new_order = $order_proc->insertNewOrder($request);
$new_order->payment_status_id = $transaction_response['response_data']['order_payment_status_id'];
$new_order->save();
// record the payment transaction
$order_proc->insertOrderPaymentData($new_order, $transaction_response);
// insert the travelers for this order
$travelers = $order_proc->insertOrderTravelers($new_order);
// insert order inbound shipment record
$order_proc->insertInboundOrderShipping($new_order->id);
// generate inbound shipping airbill
$order_proc->generateInboundShippingAirbill($new_order->id);
/// generate the invoive
$order_proc->generateInvoice($new_order);
// send new order notification to the user
$order_proc->sendNewOrderNotificationToUser($new_order);
// send new order notification to admin
$order_proc->sendNewOrderNotificationToAdmin($new_order);
// finally kill the session variable
$_SESSION['travelers'] = [];
}
[ my previous non-asynchronous implementation looks like this...]
public function processTransaction(Request $request) {
// :: POST
// Process the Authorize.net transaction, insert the order, generate invoices
// and airbills, send notifications
$order_proc = new OrderProcessingTools;
$transaction_response = $order_proc->processTransaction($request);
if($transaction_response['success'] === true) {
// insert a new order
$new_order = $order_proc->insertNewOrder($request);
$new_order->payment_status_id = $transaction_response['response_data']['order_payment_status_id'];
$new_order->save();
// record the payment transaction
$order_proc->insertOrderPaymentData($new_order, $transaction_response);
// insert the travelers for this order
$travelers = $order_proc->insertOrderTravelers($new_order);
// insert order inbound shipment record
$order_proc->insertInboundOrderShipping($new_order->id);
// generate inbound shipping airbill
$order_proc->generateInboundShippingAirbill($new_order->id);
/// generate the invoive
$order_proc->generateInvoice($new_order);
// send new order notification to the user
$order_proc->sendNewOrderNotificationToUser($new_order);
// send new order notification to admin
$order_proc->sendNewOrderNotificationToAdmin($new_order);
// finally kill the session variable
$_SESSION['travelers'] = [];
}
// either good news or bad news at this point..
return json_encode($transaction_response);
}
When I try it this way, this is the error that is returned...
xception: "Symfony\Component\Debug\Exception\FatalThrowableError"
file: "F:\wamp64\www\uspassports\public_html\app\Http\Controllers\OrderCheckoutController.php"
line: 105
message: "Argument 2 passed to App\Http\Controllers\OrderCheckoutController::processPaymentAndOrderInsertion() must be callable or null, string given
You need to pass a callable type, but passing just the string name of the method won't work as PHP will only check if it's a global function.
You need to pass an array, with the first parameter being the object to call the method on, and the second the name of the function, like so:
return self::processPaymentAndOrderInsertion($request, [$this, 'createOrder']);
Documentation: https://www.php.net/manual/en/language.types.callable.php

Strange behaviour in WordPress MySQL insert of JSON encoded data

I'm looking at both my code and my result and I don't see any glaring error, so I thought it could be useful to have a few extra sets of eyes look it over.
I have a custom PayPal IPN listener that updates a transaction table in the database. I deployed it Friday before leaving work, and after returning today it seems to be working mostly correctly; but I would like to figure out why one insert behaved strangely.
Here's a capture of the inserts which happened over the weekend:
As you can see, the intended JSON value for the log column of the 4th transaction is empty. I find it strange because the value of the transaction_id column is being parsed from the same array.
Here is the relevant db insert code:
// Generate valid IPN log
private function generateIpnLog () {
global $wpdb;
// prepare log
$array_log = [];
$array_log['verified'] = true;
$array_log['ipn_response'] = (isset($this->PayPal_Response)) ? : 'Error reading from POST array';
// Parse transaction ID
$transaction_id = (isset($this->PayPal_Response['txn_id'])) ? $this->PayPal_Response['txn_id'] : null;
// Generate log
$log = json_encode($array_log);
// Update DB
$wpdb->insert(
'log_paypal',
[
'transaction_id' => ($transaction_id) ? $transaction_id : 'Error getting transaction ID',
'log' => ($log) ? $log : 'Error generating transaction log'
],
[
'%s',
'%s'
]
);
// json log response
$this->json_return = $log;
}
Seeing as the transaction id is parsed fine from the PayPal response, and because we know $array_log['verified'] has an explictly declared value my guess is there must be a problem with json_encode($array_log).
I also checked the data from PayPal IPN history of the PayPal account in question, and can verify there isn't anything different about the way the data is being formed in the null log column vs the others.
Anyone have an idea about what could be happening in this instance?
As suggested by #ishegg it was an encoding issue since PayPal IPN uses windows-1252 encoding and the DB field was encoded in UTF-8.
It was easy to fix in this case since the PayPal return data is not nested / multidimensional (see below).
Called in an earlier function after a PayPal IPN entry is cryptographically verified by the cert chain:
// Process IPN response
$this->PayPal_Response = $this->processIpn();
Then, the function itself:
// Manipulate IPN response and prepare
// for database update and log
private function processIpn () {
// Response ref.
$response = $this->post_array;
if ( isset($response['charset']) ) {
if ($response['charset'] == "windows-1252") {
foreach ($response as $key => $ipn_value) {
$response[$key] = mb_convert_encoding($ipn_value, 'UTF-8', 'Windows-1252');
}
}
}
return $response;
}

Stripe doubles anything

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.

In Braintree is it possible to verify duplicate payment method for just one customer instead of entire vault?

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.

PayPal - Get transaction details for recurring profile

On making the TransactionSearch request I receive the list of the transactions with the TRANSACTIONID field for the transactions, corresponding to the recurring payments, in the form e.g. "I-BRPN2RUD8W0G" (current is fake).
For the rest transactions - I get usual 17 single-byte alphanumeric string. That means, that for recurring payments PayPal returns ProfileID, but not TransactionID.
As a result when I request the GetTransactionDetails with this transaction id passed to PayPal I receive valid details for ordinary payments and ERROR with the message "The transaction id is not valid" for the case of recurring payments.
You will need to set IPN as suggested by Sanjiv. You can get the fields as per IPN Variables. In case of refund you will also need to use parent_txn_id
If you are new with this and finding tough, you can use IPN listener class and then integrate below code
$listener = new IpnListener();
try {
$verified = $listener->processIpn();
} catch (Exception $e) {
return Log::error($e->getMessage());
}
if ($verified) {
$data = $_POST;
$user_id = json_decode($data['custom'])->user_id;
$subscription = ($data['mc_gross_1'] == '10') ? 2 : 1;
$txn = array(
'txn_id' => $data['txn_id'],
'user_id' => $user_id,
'paypal_id' => $data['subscr_id'],
'subscription' => $subscription,
'expires' => date('Y-m-d H:i:s', strtotime('+1 Month')),
);
Payment::create($txn);
} else {
Log::error('Transaction not verified');
}
Save this file code in file let say, ipn.php and now assign web path for this file in your paypal account.
PS: make sure your IPN file is on publicly accessible URL. Do not use local or restricted server.
You have to set IPN in your Paypal merchant account (specially for recurring payments), Which sends you back a transaction details when a recurring payment happens, from there you can get $_POST['txn_id'] which is your TRANSACTIONID if $_POST['txn_type'] is recurring_payment. Save the details in your database and then you can call GetTransactionDetails method when you need transaction details. More

Categories