] * * @see ComponentCheckoutController::store() method that stores the data. * @since [v3.0] * * @param int $id * @return View * * @throws AuthorizationException */ public function create($id) { if ($component = Component::find($id)) { $this->authorize('checkout', $component); // Make sure the category is valid if ($component->category) { // Make sure there is at least one available to checkout if ($component->numRemaining() <= 0) { return redirect()->route('components.index') ->with('error', trans('admin/components/message.checkout.unavailable')); } // Return the checkout view return view('components/checkout', compact('component')) ->with('snipe_component', $component); } // Invalid category return redirect()->route('components.edit', ['component' => $component->id]) ->with('error', trans('general.invalid_item_category_single', ['type' => trans('general.component')])); } // Not found return redirect()->route('components.index')->with('error', trans('admin/components/message.not_found')); } /** * Validate and store checkout data. * * @author [A. Gianotto] [] * * @see ComponentCheckoutController::create() method that returns the form. * @since [v3.0] * * @param int $componentId * @return RedirectResponse * * @throws AuthorizationException */ public function store(Request $request, $componentId) { // Check if the component exists if (! $component = Component::find($componentId)) { // Redirect to the component management page with error return redirect()->route('components.index')->with('error', trans('admin/components/message.not_found')); } $this->authorize('checkout', $component); $max_to_checkout = $component->numRemaining(); // Make sure there are at least the requested number of components available to checkout if ($max_to_checkout < $request->input('assigned_qty')) { return redirect()->back()->withInput()->with('error', trans('admin/components/message.checkout.unavailable', ['remaining' => $max_to_checkout, 'requested' => $request->input('assigned_qty')])); } $validator = Validator::make($request->all(), [ 'asset_id' => 'required|exists:assets,id', 'assigned_qty' => "required|numeric|min:1|digits_between:1,$max_to_checkout", ]); if ($validator->fails()) { return redirect()->back() ->withErrors($validator) ->withInput(); } // Check if the asset exists $asset = Asset::find($request->input('asset_id')); if (! $component->canCheckoutTo($asset)) { return redirect()->route('components.checkout.show', $componentId)->with('error', trans('general.error_checkout_company_mismatch', [ 'item' => trans('general.component').' "'.$component->name.'"', 'item_company' => $component->company?->name ?? trans('general.unassigned'), 'target' => trans('general.asset').' "'.$asset->display_name.'"', ])); } $component->checkout_qty = $request->input('assigned_qty'); // Concurrency guard. The numRemaining() check above is an unlocked // read, so two simultaneous checkout requests could both pass, both // attach a pivot row, and land the register at -1. Re-fetch the // parent under lockForUpdate INSIDE a transaction, re-check against // the locked snapshot, then write. Mirrors the License checkout // locking pattern. $overAllocated = false; DB::transaction(function () use ($component, $request, &$overAllocated): void { $locked = Component::whereKey($component->id)->lockForUpdate()->first(); if (! $locked || $locked->numRemaining() < $component->checkout_qty) { $overAllocated = true; return; } $component->asset_id = $request->input('asset_id'); $component->assets()->attach($component->id, [ 'component_id' => $component->id, 'created_by' => auth()->user()->id, 'created_at' => date('Y-m-d H:i:s'), 'assigned_qty' => $component->checkout_qty, 'asset_id' => $request->input('asset_id'), 'note' => $request->input('note'), ]); }); if ($overAllocated) { return redirect()->back()->withInput()->with('error', trans('admin/components/message.checkout.unavailable', [ 'remaining' => $component->fresh()->numRemaining(), 'requested' => $component->checkout_qty, ])); } event(new CheckoutableCheckedOut( $component, $asset, auth()->user(), $request->input('note'), [], $component->checkout_qty, )); $request->request->add(['checkout_to_type' => 'asset']); $request->request->add(['assigned_asset' => $asset->id]); session()->put(['redirect_option' => $request->input('redirect_option'), 'checkout_to_type' => $request->input('checkout_to_type')]); return Helper::getRedirectOption($request, $component->id, 'Components') ->with('success', trans('admin/components/message.checkout.success')); } }