Laravel transaction not rolling back - php

I'm trying to use Laravel transactions for an operation that requires a change in customer's balance.
In this specific situation, when an already approved (status = 1) process is edited, the update operation must subtract the current process amount and add the new process amount to the customer's wallet.
This is the current code:
$process = Process::findOrFail($id);
// CHECKS THE CURRENT PROCESS SUBSIDY
$old_subsidy = $process->subsidy;
try {
DB::beginTransaction();
$process->update($request->all());
// SUMS NEW PROCESS SUBSIDY ONLY WHEN PROCESS IS ALREADY APPROVED
if($process->status == 1) {
$customer = Customer::findOrFail($process->customer_id);
$customer->balance = $request->subsidy - $old_subsidy;
$customer->save();
}
DB::commit();
$notification = array(
'title' => __('Success'),
'message' => __('Process updated with success'),
'class' => 'bg-success'
);
return redirect()->route('processes.show',$process->id)->with($notification);
} catch (\Exception $e) {
DB::rollback();
$notification = array(
'title' => __('Erro'),
'message' => __('Ocorreu um erro'),
'class' => 'bg-danger'
);
return redirect()->back()->with($notification);
}
To test, i'm forcing an error, on this line, requesting a customer id that doesn't exist, like: $customer = Customer::findOrFail(99999). The error message is shown but the changes in the process are still being saved instead of roled back.
Am i missing something here?
Thanks in advance!

Related

How to make sure I don't oversell tickets with Laravel and Stripe?

I have a Laravel 9 app, where admins can create events and a limited number of tickets, with different conditions (this means an event can have multiple types of tickets, with different conditions and different prices), then, users can buy those tickets.
I want to make sure each user can buy only one ticket for each event, and assure that if payment is made, this user will receive their ticket.
My concerns are that I need to make sure that each ticket is reserved to one user and one user only at a time. After that, I want to make sure every time user has completed payment, that his reserved ticket is marked as sold, no matter what.
I know I should be using some sort of webhook as a callback, but I couldn't figure out what events should i listen to, or how to set up properly those webhooks in my Laravel App.
If anyone knows any way to do it better, I am completely open to a redesign.
I currently have this setup, where each type of ticket belongs to one event. Then, when admin publishes that ticket for n people, n unique sellable tickets (usts) are created (this is each and every one of the individual tickets people can buy, with a unique identifier).
When a user wants to buy one ticket, I have a controller that first reserves that ticket for that user.
Check out if is there any unique sellable ticket (ust) that is currently not sold or reserved to anyone.
If there is any avaiable ust, create Stripe Checkout Session and store Stripe Session Id to that ust object in database.
After payment is completed, the return url calls another method that does the next:
3.1. Find any unique sellable tickets reserved to user, but not yet marked as sold.
3.2. Foreach ticket that logged in user has reserved but not sold, check out in Stripe api if payment has been completed.
3.3 If payment has been completed, check sold as true.
Notes: I have set up stripe Checkout to expire in 30 minutes, so thats why i select any usthat is marked as not sold and reserved more than 30 minutes ago as available.
These are my reserving ticket and Stripe Callback functions so far:
public function ReserveEventTicket($event, $ticket){
try {
$event = Event::where('slug', $event)->firstOrFail();
} catch (\Throwable $th) {
return back()->withErrors(['Evento no encontrado']);
}
if(Carbon::createFromFormat('Y-m-d H:i:s', $event->end_datetime)->isPast()){
return back()->withErrors(['Este evento ya ha terminado.']);
}
try {
$ticket = Ticket::where('slug', $ticket)->where('event_id', $event->id)->firstOrFail();
} catch (\Throwable $th) {
return back()->withErrors(['Entrada no encontrada']);
}
//El usuario no ha comprado la entrada.
foreach ($event->tickets as $us_ticket) {
$AlreadyBoughtFound = $us_ticket->usts->where('receiver_id', Auth::user()->id);
if($AlreadyBoughtFound->count() != 0){
return back()->withErrors(['¡No tan rápido! No puedes comprar otra entrada para este evento.']);
}
}
//Reservar ticket
try {
$UST = Ust::where('ticket_id', $ticket->id)
->where(function($q){
$q->where('reserved_datetime', '<', Carbon::parse()->subMinutes(32));
$q->orWhere('reserved_datetime', null);
})
->where('sold', false)->lockForUpdate()->firstOrFail();
} catch (\Throwable $th) {
return back()->withErrors(['No quedan entradas disponibles.']);
}
try {
DB::transaction(function () use($UST) {
$reserveUST = $UST->update([
'reserved' => true,
'reserved_to' => Auth::user()->id,
'reserved_datetime' => Carbon::now()->format('Y-m-d H:i:s')
]);
if(\Session::has('referral')){
$pr = PrVenue::where('code', \Session::get('referral'))->get()->first();
if($pr){
if($pr->venue_id == $UST->ticket->event->venue->id){
$addReferralSold = ReferralTransaction::create([
'ticket_id' => $UST->id,
'user_id' => $pr->user_id,
'distributor' => $pr->distributor,
]);
}
}
}
});
} catch (\Throwable $th) {
return back()->withErrors(['Error reservando la entrada'.$th]);
}
try {
if($ticket->pay_at_door){
if(config('pricing.payments.pay_at_door.style') == "variable"){
$stripe = new \Stripe\StripeClient(config('services.STRIPE_SECRET_KEY'));
$price = $stripe->prices->retrieve($ticket->stripe_price_id,[]);
$application_fee = round(config('pricing.pamyments.pay_at_door.amount')*$price->unit_amount*100, 0);
}else if(config('pricing.payments.pay_at_door.style') == "fixed"){
$application_fee = config('pricing.pamyments.pay_at_door.amount');
}
}else{
$application_fee = 140;
}
\Stripe\Stripe::setApiKey(config('services.STRIPE_API_KEY'));
$session = \Stripe\Checkout\Session::create([
'line_items' => [[
'price' => $ticket->stripe_price_id,
'quantity' => 1,
]],
'mode' => 'payment',
'success_url' => "https://phplaravel-843314-2906175.cloudwaysapps.com/events/SucessCallback?session_id={CHECKOUT_SESSION_ID}",
'cancel_url' => url('events/SucessCallback?session_id={CHECKOUT_SESSION_ID}'),
'client_reference_id' => $UST->id,
'expires_at' => time() + (60 * 30),
'payment_intent_data' => [
'application_fee_amount' => $application_fee,
'transfer_data' => [
'destination' => $event->organization->stripe_user_id
],
],
]);
DB::transaction(function () use($session, $UST) {
$associateUST = $UST->update([
'stripe_session_id' => $session->id
]);
});
} catch (\Throwable $th) {
return back()->withErrors(['Error en la pasarela de pago. Volver a intentar.'.$th]);
}
return redirect($session->url);
}
public function SuccessfullPaymentCallback(Request $req){
$stripe = new \Stripe\StripeClient(config('services.STRIPE_API_KEY'));
$session = $stripe->checkout->sessions->retrieve(
$req->session_id,[]);
//Check if session has been completed and if user matches.
if($session->payment_status == 'paid'){
try {
DB::transaction(function () use($session) {
$UST = Ust::where('reserved', true)->where('stripe_session_id', $session->id)->where('id', $session->client_reference_id)->firstOrFail();
$update = $UST->update([
'receiver_id' => Auth::user()->id,
'sold' => true
]);
$NewUST = $UST->refresh();
$updateReferralSold = ReferralTransaction::where('ticket_id', $UST->id)->update([
'sold' => true,
]);
});
$ust = User::find(Auth::user()->id)->usts->where('stripe_session_id', $session->id)->first();
$tick = array();
$tick['code'] = $ust->code;
$tick['event_name'] = $ust->ticket->event->name;
$tick['event_link'] = url('events/'.$ust->ticket->event->slug);
$tick['event_image'] = ImageController::ShowImage($ust->ticket->event->poster);
$tick['venue_name'] = $ust->ticket->event->venue->name;
$tick['zone'] = $ust->ticket->zone->name ?? '-';
$tick['ticket'] = $ust->ticket->name;
$tick['limit_time'] = ($ust->ticket->time_limit) ? $ust->ticket->max_datetime : '-';
//Revisar esto bien
Storage::put('public/ust_'.$tick['code'].'.png', \QrCode::format('png')->merge('/public/images/icons/icon-128x128bg.png')->size(400)->generate($tick['code']));
Mail::send('mails.TicketEmail', ["tick"=>$tick], function($message)use($tick) {
$message->to(Auth::user()->email, Auth::user()->email)
->subject('Entrada para '.$tick['event_name']);
});
//Mail::to([])->send(new TicketEmail($ust->ticket->id, $ust->code, $ust->stripe_session_id));
} catch(\Throwable $th) {
Storage::put('file.txt', $th);
Storage::put('file.png', \QrCode::format('png')->merge('/public/images/icons/icon-128x128bg.png')->size(400)->generate('dsalfjadshñfjah'));
return back()->withErrors(['No se ha completado el proceso de adquisición de tu entrada. Vuelve a iniciar sesión para arreglar el problema.']);
}
}else{
try {
$UST = Ust::where('reserved', true)->where('stripe_session_id', $session->id)->where('id', $session->client_reference_id)->update([
'reserved' => false,
'stripe_session_id' => null,
'reserved_to' => null,
'reserved_datetime' => null,
'receiver_id' => null
]);
} catch(\Throwable $th) {
return back()->withErrors(['El pago no se ha completado y no se ha adquirido tu entrada.']);
}
}
return redirect(url('me/wallet'))->with('success', 'Compra completada correctamente.');
}
After payment is completed, the return url calls another method that does the next: 3.1. Find any unique sellable tickets reserved to user, but not yet marked as sold. 3.2. Foreach ticket that logged in user has reserved but not sold, check out in Stripe api if payment has been completed. 3.3 If payment has been completed, check sold as true.
If you implement webhook events, you won't have to loop over each ticket in your system. Instead, you'll automatically get a checkout.session.completed webhook event from Stripe that will inform you of which ticket has been purchased. So in your webhook handler code, you can mark the ticket as "sold".
I'm not a Laravel expert but here are a few resources on how you can handle webhooks [0] [1]. The concept is basically that Stripe sends you a POST request with an Event object in the request body. You set up an endpoint where Stripe sends that request and you return an HTTP response to it.
[0] https://github.com/spatie/laravel-stripe-webhooks
[1] https://laracasts.com/discuss/channels/general-discussion/stripe-webhooks-with-laravel

Check if update happened in put request

I am new at PHP. We are creating REST API in Phalcon and I've created a put request. It already works, but I would like to check if update has really happened before sending a success response. So I've created a conditional for that ( if (!$product->update()) ), but it always returns 'true'. How can I check if any field has changed in a record?
public function put()
{
$id = $this->getParam('id');
$input = $this->getRawData();
$product = Product::findFirst([
'conditions' => 'id = :id:',
'bind' => ['id' => $id]
]);
if ($product === null){
throw new NotFoundException();
}
$product->assign($input);
$product->update();
if (!$product->update()) {
$this->errorResponse($product->getMessages());
} else {
$this->successResponse($product->toArray($product->update()));
}
}
You can use Model Events, i.e. afterUpdate and notSaved, like:
use Phalcon\Mvc\Model;
use Phalcon\Http\Response;
class ModelBase extends Model
{
public function afterUpdate()
{
$response = new Response();
$response->setJsonContent([
'success' => true,
'message' => "Record updated"
])->send();
}
public function notSaved()
{
$response = new Response();
$response->setJsonContent([
'success' => false,
'message' => 'Record not saved'
])->send();
}
}
The Product and all other models will extend ModelBase. Then your code could be:
public function put()
{
$id = $this->getParam('id');
$input = $this->getRawData();
$product = Product::findFirst([
'conditions' => 'id = :id:',
'bind' => ['id' => $id]
]);
if ($product === null){
throw new NotFoundException();
}
$product->assign($input);
$product->update();
}
And Phalcon event will respond if the model was updated or not. If you prefer, you can also use custom http response codes for update or notSaved. More information about Model Events in the documentation
You are calling $product->update() three times. You do it once after the assign, then again for your if test, which is why it's always returning TRUE there I believe, and once inside the toArray() which may not actually return anything since the second and third updates don't have any data to update (not sure about that though).
I would code this as follows:
$product->assign($input);
$results = $product->update();
if (!results) {
$this->errorResponse($product->getMessages());
} else {
$this->successResponse($results->toArray());
}
I am assuming that the $product->assign($input); statement is working as expected to update the $product data for you. I don't use that. I prefer to do direct assignments for updates so nothing is left to chance, ie. $product->whatever = $input['whatever'];.
Give this a try and hopefully it will work as expected for you.

How to add and update in one api in laravel

I am trying to add and update using the same API, currently, I can add but I am not clear about how to update using the same API.
I am adding folders against id and my body response looks like this:
{
"id": "2",
"folder_detail": [1,3,4]
}
I can add folders with id 1,3 and 4 against id 2 but next time when I hit the same API with folder[1,3,5] it should update folders details not should add again, I can do that by making separate API but I want to do that in one API.
My Controller code:
try {
$folder = Doctor::where('id', $request->get('id'))->first();
$folder->doctor()->attach($request->get('folder_detail', []));
DB::commit();
return response([
'status' => true,
'message' => 'Folder detail added',
], 200);
} catch (\Exception $ex) {
DB::rollback();
return response([
'status' => false,
'message' => __('messages.validation_errors'),
'errors' => $ex->getMessage(),
], 500);
}
}
public function doctor()
{
return $this->belongsToMany('App\Folder', 'folder_details');
}
Your help will be highly appreciated?
Okay so after our back and forth in the comments I think you are looking for something like this:
$folders = $request->get('folder_detail', []);
foreach($folders as $folder) {
$record = Folder::firstOrNew(['id', $folder]);
$record->doctor_id = $request->id;
// You can add your own records here, or use the 'fill' function
}
So, this way, you loop through all your folders. Check if the folder with the specific ID already exists, if it does not, it creates a new one. The doctor is linked through the doctor_id on your record.
Find record if exist then update otherwise create
$post = $request->all();
$doctor = Doctor::find($post['id']);
if($doctor){
$doctor->update([
//your data for update doctor model
]);
//remove old folders which is related with this doctor
Folder::where('doctor_id', $doctor->id)->delete();
//add current data of folder
if(!empty($post['folder_detail'])){
foreach($post['folder_detail'] as $folder){
Folder::create([
'doctor_id' => $doctor->id,
'folder_detail' => $folder
]);
}
}
//return your response here
} else {
$doctor = Doctor::create([
//data for create doctore
]);
//add current data of folder
if(!empty($post['folder_detail'])){
foreach($post['folder_detail'] as $folder){
Folder::create([
'doctor_id' => $doctor->id,
'folder_detail' => $folder
]);
}
}
//return your response here
}

Duplicate records are created in case of concurrent requests

I have two tables orders and driver_orders.The sample code is :
DB::beginTransaction();
try{
//check if the order status is pending.If the status is other than pending then one of the driver has accepted the order.
$fetch_order = $this->order->where([
['id',$id],
['status','pending']
])->lockForUpdate()->first();
if(!fetch_order){
throw new Exception('Order is already assigned to another driver.');
}
$driver_id = Auth::id();
//checks if logged in driver has pending order to be delivered.
$check_user_pending_order = $this->driver_order->where([
['driver_id', $driver_id ],
['status','!=','delivered']
])->lockForUpdate()->first();
if($check_user_pending_order){
throw new Exception('You have pending order to be delivered.');
}
$data['order_id'] = $fetch_order->id;
$data['driver_id'] = $driver_id;
$data['amount'] = $fetch_order->amount;
$data['status'] = 'assigned';
//create record in driver_order table
$this->driver_order->create($array);
//update the status in order table
$fetch_order->update([
'status' => 'assigned'
]);
DB::commit();
return response()->json([
'status' => '200',
'message' => 'Order successfully assigned. Get ready to deliver the order.'
]);
}
catch(\Exception $ex){
return response()->json([
'status' => '403',
'message' => $ex->getMessage()
],403);
}
The issue is I am getting concurrent request which tends to create duplicate records for the same order in driver_orders table. I used the locking but it does not solve the problem as well. Please suggest me the way to tackle the above mention issue.

Implementing Payment Gataway in Laravel based shop

I need a little help with implementing payment getaway in Laravel shop.
Payment I use is https://gourl.io/ and I can't understand how to take needed information. So I have set the files database table, database connection and all.. Now I'm trying to redirect user to payment.php page after order form is submitted. This is my CartController.php orderSubmit function
public function orderSubmit() {
$cart = Session::get(self::CART_SESSION_KEY, array());
if (count($cart) < 1) {
return Redirect::to('/');
}
$validatorRules = array(
'captcha' => 'required|captcha',
'shipping_address' => 'required|min:10',
'shipping_method' => 'required|in:' . implode(',', [Settings::SETTINGS_SHIPPING_NORMAL, Settings::SETTINGS_SHIPPING_EXPRESS])
);
Input::merge(array_map('trim', Input::all()));
$validator = Validator::make(Input::all(), $validatorRules);
if ($validator->fails()) {
return Redirect::to('/cart/order?_token=' . csrf_token())->withErrors($validator->errors())->withInput(Input::except(['captcha']));
}
$shipping = array(
'quantity' => 1,
'image' => '/img/noimage.png',
'description' => '',
'title' => 'FIX ME', // this should never occur,
'price' => 100000 // this should never occur
);
switch (Input::get('shipping_method')) {
case Settings::SETTINGS_SHIPPING_NORMAL:
$shipping['title'] = 'Normal Delivery';
$shipping['price'] = 0;
break;
case Settings::SETTINGS_SHIPPING_EXPRESS:
$shipping['title'] = sprintf('Express Delivery - $%.2f', Settings::getOption('express_shipping_cost'));
$shipping['price'] = doubleval(Settings::getOption('express_shipping_cost'));
break;
}
$cart['shipping'] = $shipping;
$order = new Order();
$order->user_id = self::$user->user_id;
$order->data = json_encode($cart);
$order->address = Input::get('shipping_address');
$order->pgp_key = Input::get('gpgkey');
$order->info = Input::get('additional_info');
$order->save();
Session::put(self::CART_SESSION_KEY, array());
return Redirect::to('payment.php')->with('message_success', 'Order created! We will contact you shortly to confirm your order and payment details.');
}
and this is payment.php
require_once( "../cryptobox.class.php" );
/**** CONFIGURATION VARIABLES ****/
$userID = ""; // place your registered userID or md5(userID) here (user1, user7, uo43DC, etc).
// you don't need to use userID for unregistered website visitors
// if userID is empty, system will autogenerate userID and save in cookies
$userFormat = ""; // save userID in cookies (or you can use IPADDRESS, SESSION)
$orderID = "";
$amountUSD = 20;
$period = "NOEXPIRY";
$def_language = "en";
$public_key = "mypublickey";
$private_key = "myprivatekey";
/** PAYMENT BOX **/
$options = array(
"public_key" => $public_key, // your public key from gourl.io
"private_key" => $private_key, // your private key from gourl.io
"webdev_key" => "", // optional, gourl affiliate key
"orderID" => $orderID, // order id or product name
"userID" => $userID, // unique identifier for every user
"userFormat" => $userFormat, // save userID in COOKIE, IPADDRESS or SESSION
"amount" => 0, // product price in coins OR in USD below
"amountUSD" => $amountUSD, // we use product price in USD
"period" => $period, // payment valid period
"language" => $def_language // text on EN - english, FR - french, etc
);
// Initialise Payment Class
$box = new Cryptobox ($options);
// coin name
$coinName = $box->coin_name();
// Successful Cryptocoin Payment received
if ($box->is_paid())
{
if (!$box->is_confirmed()) {
$message = "Thank you for payment (payment #".$box->payment_id()."). Awaiting transaction/payment confirmation";
}
else
{ // payment confirmed (6+ confirmations)
// one time action
if (!$box->is_processed())
{
// One time action after payment has been made/confirmed
$message = "Thank you for order (order #".$orderID.", payment #".$box->payment_id()."). We will send soon";
// Set Payment Status to Processed
$box->set_status_processed();
}
else $message = "Thank you. Your order is in process"; // General message
}
}
else $message = "This invoice has not been paid yet";
$languages_list = display_language_box($def_language);
My question is how to take the correct info in the payment.php? How to take userID, userFormat, orderID and so on?
First of all, I would suggest you use Laravel as the framework it is intended for. In Laravel you define controllers to handle your http-requests. Make a new PaymentController and put the code from payment.php into this controller. Then make a route to that controller-method.
Also put your configuration settings in Laravels config-folder.
And the require_once( "../cryptobox.class.php" ); can be replaced by a dependency injection in your controllers constructor.
Now back to your question.
$userID is where you put your registered Laravel user ID. (If you dont have any registered users, leave it blank). Why you should put your user's id in this variable? -It helps to keep track of which users have done which payments. You can later save this information in your database if you want to keep track of payment history.
$orderID This is where you put your internal order id. Why should you use an internal order id? -Again its to keep track of which purchases of which products have been done by which users. You can store your order-id in your database together with user-id and product-id to get a purchase history log.
$userFormat This is how you wish to store your user information, session, cookie, etc. Because when the purchase is executed, the payment gateway needs a way to access this information, and therefor it must be stored in the session or in a cookie.
I would use $_SESSION['$value'] if you use session for your users!

Categories