I’ve been using laravel for a while now, but till now I have not worked in a concurrent enviornment, maybe a few times. Right now I am developing a function which is going to execute a payment. On this stage it is kind of a draft, where I am thinking of different situations that might occur and how they should be handled.
Inside the function the product quantity plays a crucial role, so I am using the DB:: and “lockForUpdate()” mechanism for the product record. I am curious about your opinion on that and do you think it is a sufficient solution concerning the data concurrency. If you have any improvement hints or just any comment concerning it, I would be very grateful if you share them with me. Thank you in advance!
public function create(Request $request)
{
// Begin transaction at the start of the function
DB::beginTransaction();
try {
// Validation rules
$validator = Validator::make($request->all(), [
'user_id' => 'required|exists:users,id',
'items' => 'required|array',
'items.*.product_id' => 'required|exists:products,id',
'items.*.quantity' => 'required|integer|min:1',
'shipping_address_id' => 'nullable|exists:addresses,id',
'billing_address_id' => 'nullable|exists:addresses,id',
'street_address' => [
'required_without:shipping_address_id',
function ($attribute, $value, $fail) use ($request) {
if (!$request->filled('shipping_address_id')) {
$fail($attribute.' is required when shipping_address_id is not provided.');
}
},
],
'city' => [
'required_without:shipping_address_id',
function ($attribute, $value, $fail) use ($request) {
if (!$request->filled('shipping_address_id')) {
$fail($attribute.' is required when shipping_address_id is not provided.');
}
},
],
'country' => [
'required_without:shipping_address_id',
function ($attribute, $value, $fail) use ($request) {
if (!$request->filled('shipping_address_id')) {
$fail($attribute.' is required when shipping_address_id is not provided.');
}
},
],
'name' => [
'required_without:shipping_address_id',
function ($attribute, $value, $fail) use ($request) {
if (!$request->filled('shipping_address_id')) {
$fail($attribute.' is required when shipping_address_id is not provided.');
}
},
],
'phone_number' => [
'required_without:shipping_address_id',
function ($attribute, $value, $fail) use ($request) {
if (!$request->filled('shipping_address_id')) {
$fail($attribute.' is required when shipping_address_id is not provided.');
}
},
],
'payment_method' => 'required|in:credit_card,cash_on_delivery',
'stripeToken' => 'required_if:payment_method,credit_card',
]);
// Additional validation for items based on payment_method
$validator->after(function ($validator) use ($request) {
foreach ($request->input('items') as $index => $item) {
$product = $this->productRepository->findProductById($item['product_id']);
$product->lockForUpdate();
// Check if product exists
if (!$product) {
$validator->errors()->add("items.$index.product_id", "Product {$item['product_id']} not found.");
continue;
}
// Check if the product can be purchased with the selected payment method
if ($request->input('payment_method') === 'credit_card' && !$product->is_payWithCard) {
$validator->errors()->add("items.$index.product_id", "Product {$item['product_id']} cannot be paid with a credit card.");
} elseif ($request->input('payment_method') === 'cash_on_delivery' && !$product->is_cashOnDelivery) {
$validator->errors()->add("items.$index.product_id", "Product {$item['product_id']} cannot be paid with cash on delivery.");
}
// Check product quantity and purchase_out_of_stock
if (!$product->purchase_out_of_stock && $product->quantity === 0) {
$validator->errors()->add("items.$index.product_id", "Product {$item['product_id']} is out of stock and cannot be purchased.");
} elseif ($item['quantity'] > $product->quantity) {
$validator->errors()->add("items.$index.product_id", "Not enough quantity available for product {$item['product_id']}.");
}
}
});
// If validation fails, return errors
if ($validator->fails()) {
DB::rollBack(); // Rollback if validation fails
return response()->json(['errors' => $validator->errors()], 400);
}
// Calculate total amount and total savings
list($totalAmount, $totalSavings, $errors) = $this->calculateTotalAmount($request->items);
// If there are errors in calculating total amount, return errors
if (!empty($errors)) {
DB::rollBack(); // Rollback if calculation errors occur
return response()->json(['errors' => $errors], 400);
}
// Handle payment based on payment method
if ($request->payment_method === 'credit_card') {
Stripe::setApiKey(env('STRIPE_SECRET'));
$charge = Stripe::createCharge([
'amount' => $totalAmount * 100,
'currency' => 'lv',
'source' => $request->stripeToken,
'description' => 'Order for user ID: ' . $request->user_id,
]);
$transactionId = $charge->id;
$paymentStatus = 'Paid';
} else {
$transactionId = null;
$paymentStatus = 'COD';
}
// Create shipping and billing addresses if not provided
$shippingAddressId = $request->shipping_address_id ?? $this->createAddress($request, 'shipping');
$billingAddressId = $request->billing_address_id ?? $this->createAddress($request, 'billing');
// Create the order
$order = Order::create([
'user_id' => $request->user_id,
'phone_number' => $request->phone_number,
'email' => $request->email,
'order_number' => $this->generateAlphanumericUuid(),
'total_amount' => $totalAmount,
'status' => 'pending',
'billing_address_id' => $billingAddressId,
'shipping_address_id' => $shippingAddressId,
'total_savings' => $totalSavings,
]);
// Create order items and update product quantities
foreach ($request->items as $item) {
$product = Product::find($item['product_id']);
// Validate quantity again to prevent overselling
if ($item['quantity'] > $product->quantity) {
DB::rollBack(); // Rollback if quantity validation fails
return response()->json(['error' => "Not enough quantity available for product {$item['product_id']}."], 400);
}
// Reduce product quantity
$product->quantity -= $item['quantity'];
$product->save();
// Create order item
OrderItem::create([
'order_id' => $order->id,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $product->price,
'total' => $product->price * $item['quantity'],
'is_onsale' => $product->is_onsale,
'sale_price' => $product->sale_price,
'total_savings' => $item['quantity'] * ($product->price - ($product->is_onsale ? $product->sale_price : $product->price)),
]);
}
// Create payment record
Payment::create([
'order_id' => $order->id,
'amount' => $totalAmount,
'payment_method' => $request->payment_method,
'transaction_id' => $transactionId,
'status' => $paymentStatus,
'total_savings' => $totalSavings,
]);
// Commit transaction after everything is successfully created
DB::commit();
return response()->json(['message' => 'Order created successfully.']);
} catch (Exception $e) {
// Roll back transaction on error
DB::rollBack();
// Refund charge if payment was processed
if (isset($charge)) {
$charge->refund();
}
// Return error response
return response()->json(['message' => 'Order creation failed: ' . $e->getMessage()], 500);
}
}