how to handle table lock or query queue - php

I got some duplicated row in my table but its impossible to insert via http request because I have validations for user balance that will change by every request and a request must be accepted by admin before another request. In this case The user had a balance of USD 1000. According to my code he can request once for USD 1000. but all of a sudden I got two rows at the same time in my database. how it may happen and what is the solution of this problem?
what I assume is when the 1st request arrived, the withdraw table was locked due to some other calculation so the balance was unchanged and the query was queued, as user got no response due to table lock he requested again then both requests were inserted!
please be informed that this is not happening everyday. It happens very rarely like 1 or 2 duplicate request in a year
public function withdrawBalance(Request $request)
{
if ($request->api_key == ApiKey()) {
$userInfo = User::find($request->user_id);
$checkPendingRequest = Withdraw::where(['user_id' => $request->user_id, 'status' => 0])->get(); //status column have default 0 until admin approval
if (count($checkPendingRequest) > 0) {
return response()->json(['status' => false, 'action' => 'already_pending_one', 'message' => 'Already have a pending request!.']);
}
$gs = GeneralSetting::first();
$balance = HoldingBalance($request->user_id)['withdrawble_balance'];
if ($request->amount > $balance) {
return response()->json(['status' => false, 'action' => 'insufficiant_balance', 'message' => 'Insufficient amount!.']);
}
if ($request->withdraw_method == 'bkash') {
$method_name = 'Digital';
} else {
$method_name = $request->withdraw_method;
}
Withdraw::create([
'branch_id' => $request->branch_id ?? 1,
'user_id' => $userInfo->id ?? null,
'purpose' => "$method_name Withdraw",
'withdraw_method' => $request->withdraw_method ?? null,
'withdraw_account' => $request->withdraw_account ?? null,
'amount' => round($request->amount, 2),
'payble_amount' => $request->amount,
'status' => 0
]);
return response()->json(['status' => true, 'action' => 'success', 'message' => 'Successfully submitted withdrawn request!.']);
} else {
return "Access Denied";
}
}

I guess any user double-click "Submit"
So 2 receipt requests arrived, each one performed a code level check and did not see that there was an execution of another query
I have not seen use of the table lock or transaction.
DB::beginTransaction();
check_if_data_already_exist();
add_data();
DB::commit();
This solution will greatly slow down the performance of your system.
The simplest solution is the option for each user to send one request at a given moment.
There are quite a few implementations of this problem.
In order to offer possible solutions we will need to get more information:
Is it possible to use the session?
Can the queue be used to enter the queries?
Maybe each user contains one row in the database and you can simply create a unique key at the database level

Related

DB::transaction is not working as exptected (Laravel)

i have 3 sql operations which i thought put them in transaction would be a good practice because many times I faced some issues like first query runs and another fails so this is my code below
DB::beginTransaction();
try {
//update loan application
$loanApplication->status = 'DISBURSED';
$loanApplication->tenure = $offerDetails['tenure'];
$loanApplication->amount_offered = $offerDetails['loanAmount'];
$isUpdated = $loanApplication->update();
//create loan disbursal
LoanDisbursal::insertGetId([
'created_dt' => Carbon::now(),
'user_id' => $loanApplication->user_id,
'loan_account' => $loanApplication->application_id,
'gateway_alias' => $this->gatewayAlias,
'amount_approved' => $tradeLineInfo['loanAmount'],
'amount_disbursed' => $tradeLineInfo['disbursalAmount'],
'disbursed_on' => $tradeLineInfo['disbursalDate'],
'disbursal_ref' => $tradeLineInfo['disbursalId'],
'tenure' => $offerDetails['tenure'],
'wallet_credit_status' => 'DISBURSED',
]);
//create loan profile account
ProfilesHasAccount::insertGetId([
'user_id' => $loanApplication->user_id,
'gateway' => $this->gatewayAlias,
'account_segment' => 'BUSINESS',
'1account_type' => 'LOAN',
'created_dt' => Carbon::now(),
'status' => 'ACTIVE',
]);
} catch (\Throwable $th) {
dd(DB::rollback());
dd($th);
}
DB::commit();
Now here is the problem ,
while doing this operation my loan applicatiob got updated but there was problem on loanDisbursal entry so my catch block was invoked ,
there I used DB::rollback ,
so according to rollback,
loanApplication status should have change as it was earlier but it is 'DISBURSED' and no rollback happened in simple words
Also I have another project setup in my system in which everything is working as expected so what maybe the cause for this ,
I guess anything related to mysql configuration?
so I am just thinking why this happened ?
any ideas or suggestions , would be very helpful
OKAY I found the answer and the problem was my projects contains multiple datbases so I just had to show connections on transactions like this
DB::connection('ipay_app_loans__write')->beginTransaction();
DB::connection('ipay_app_loans__write')->rollBack();
DB::connection('ipay_app_loans__write')->commit();
That's it!!

How to validate a post request without a specific attribute to validate

I have a time tracking application where every time that a new Time Entry is about to be added, I must first verify that all the previous time entries have been closed (meaning that an ending date has been set) and throw and error message using the validate() method.
I don't know how feasable this is or how to do it, reading the documentation it seems that most custome rules require that an attribute be given, but in this case it's more about validating the logical requirements rather than the form of the post request.
When I receive a post request I fetch all previous time entries which come before the post request starting time and have not yet been given an ending time.
Ideally, if I get any time entries returned I would throw an error saying 'You need to close the previous time entry before opening a new one'.
For more clarity, here is what I want to do in code :
$timeEntry= new TimeEntry;
$openTimeEntries = $timeEntry->Where('start_time', '<', $request->startTime)->Where('end_time', 0)->get();
$count = $openTimeEntries->count();
$request->validate([
'comment' => 'string',
'candidateId' => 'required',
'startTime' => 'required|date',
'endTime' => 'date|nullable|after:startTime',
'CustomeTimeEntryRule' => $openTimeEntries->count() > 0, // If false I want this rule to add the message to the validate error array
]);
You are on the right track.
However, If you really customize validation you should create a request for here you can read more about it.
Simply call php artisan make:request TimeEntryStoreRequest
public function rules()
{
return [
'CustomeTimeEntryRule' => $openTimeEntries->count() > 0,
];
}
/**
* #return array|string[]
*/
public function messages(): array
{
return [
'CustomeTimeEntryRule.*' => 'Custom message',
];
}
However, if it is not a form input from a user I think you should check it inside your controller not in the form.
Also you can simplify your code like this:
use App\Models\TimeEntry;
$openTimeEntriesCount = TimeEntry::select('id')->where('start_time', '<', $request->startTime)->where('end_time', 0)->count();
A simple way to do this is to merge the custom attribute to the request :
$timeEntry= new TimeEntry;
$openTimeEntries = $timeEntry->Where('start_time', '<', $request->startTime)->Where('end_time', 0)->get();
$count = $openTimeEntries->count();
$request->merge([
'CustomeTimeEntryRule' => $count,
]);
Then we can validate the attribute using the in rule, which will return a custom validation message which we can specify as a second argument, when the count is not equal to 0:
$request->validate([
'comment' => 'string',
'candidateId' => 'required',
'startTime' => 'required|date',
'endTime' => 'date|nullable|after:startTime',
'CustomeTimeEntryRule' => 'in:0',
], [
'CustomeTimeEntryRule.in' => 'You need to close the previous time entry before opening a new one'
]);

Laravel database notifications update count only

I have an application build in Laravel where it sends notifications to users.
for Example when a new Post is created it sends notification to multiple users based on some conditions, the problem I'm facing is when multiple posts are create in small period of time for example 30 Minutes I don't want the user to get notified by lots of notifications,
I just want to update the count in last notification.
but I need to prevent creation of a new notification when I update the last notification.
here is what I have written.
public function toDatabase($notifiable): array
$count = 1;
if($notification = $notifiable->notifications()->where('data->search', $this->search->id)
->where('updated_at', '>=', now()->subMinutes(30))
->first()) {
$count = isset($notification->data['count']) ? $notification->data['count'] + 1 : 1;
$notification->update([
'data' => [
'content' => [
'en' => "{$count} new posts matched with your saved search {$this->search->title} has been posted, Press here to view more.",
],
'count' => $count
]
]);
}
return [
'content' => [
'en' => "{$count} new post matched with your saved search {$this->search->title} has been posted, Press here to view more.",
],
'count' => $count,
'search' => $this->search->id,
'parameters' => $this->search->parameters
];
}
How to prevent the code from reaching to return [] ?
maybe add another condition, maybe in your database add an column Last_send
and when executing it compares the current time from Last_send if time is greater than 30min you let it pass.

How would I keep tracking of paginated value in my controller and display a certain amount of records per page?

I'm getting paginated values from a query that lives inside a repository. However, in my controller (snippet below) - I want to keep track of the current paginated page the user's on and would like to display records if, say, the user's on page 1, then get items 1-30.
(30 entries will be the max allowed on one paginated page or less than 30 if there aren't enough records)
If the user's on page 2, then get 31-60 and so on. How would I go about setting the logic for that in my controller? I've tried too many things to mention, I just need another pair of eyes.
Here's controller code:
try {
$inventoryList = $this->_inventoryRepository->getInventories();
if($inventoryList['current_page'] <= 0) {
$inventoryList['current_page'] = 1;
}
$inventoryList['per_page'] = 30; // 30 entries per page
$inventoryList['current_page'] = (int) get_value($inventoryList["current_page"], 1); // the current page, default will be 1
$totalRecords = $inventoryList['total']; // total number of records 88
// now I have all records
// if page is 2 and total records for page is 30, then acquire records from 31-60
return [
'isValid' => true,
'list' => $inventoryList,
'page' => $inventoryList['current_page'],
'totalRecords' => $totalRecords,
'pageSize' => $inventoryList['per_page'],
'message' => 'Successfully fetched inventories list'
];
} catch( Exception $exception) {
return [
'isValid' => false,
'code' => $exception->getCode(),
'systemMessage' => $exception->getMessage(),
'message' => "Something went wrong"
];
}
Here's the paginated query:
$this->model
->distinct()
->with('table.rewards')
->select($attributes['inventory'])
->paginate();

Amount must be greater than zero, PayPal/Braintree payment

I have an app that sells events, some events have zero-dollar price. Thus, I receive this message from PayPal:
Amount must be greater than zero
However, if I remove the need to make a payment, one user can order all the tickets for an event. So, what's the way to make users go through the payment process without making a charge.
Bear in mind, it'd be great to make as little change to the process in terms of generating a Nonce from frontend and sending it to backend to make the charge it.
I'm using this payment scenario.
This is the function that returns the error message:
public function chargeNonce($nonce) {
$gateway = $this->createPayPalGateway();
$amount = $this->payment_params['number_of_tickets'] * $this->payment_params['event_price'];
$result = $gateway->transaction()->sale([
'amount' => $amount,
'paymentMethodNonce' => $nonce,
'options' => [
'submitForSettlement' => True,
'paypal' => [
'description' => "Billing Notification",// For email receipt
],
],
]);
dd($result);
return $result;
}

Categories