validate([ 'order_type' => ['required', 'in:dine_in,takeaway,counter'], 'table_id' => ['nullable', 'integer'], 'covers' => ['nullable', 'integer', 'min:1', 'max:99'], ]); $merchant = ladill_account() ?? $request->user(); $location = $this->locations->ensureDefault($this->ownerRef($request)); try { $sale = $this->sales->openTicket($merchant, [ 'order_type' => $data['order_type'], 'table_id' => $data['table_id'] ?? null, 'covers' => $data['covers'] ?? null, 'location_id' => $location->id, 'currency' => $location->currency, ]); } catch (RuntimeException $e) { return back()->with('error', $e->getMessage()); } return redirect()->route('pos.tickets.show', $sale); } public function show(Request $request, PosSale $sale): View { $this->authorizeOwner($request, $sale); if (! $sale->isOpen()) { return redirect()->route('pos.sales.show', $sale); } $owner = $this->ownerRef($request); $sale->load('lines', 'table'); $products = PosProduct::owned($owner)->active()->orderBy('name')->get(); return view('pos.tickets.show', [ 'sale' => $sale, 'products' => $products, 'lines' => $this->lineData($sale), ]); } public function addLine(Request $request, PosSale $sale): JsonResponse { $this->authorizeOpen($request, $sale); $data = $request->validate([ 'product_id' => ['nullable', 'integer'], 'name' => ['required', 'string', 'max:200'], 'unit_price_minor' => ['required', 'integer', 'min:1'], 'quantity' => ['nullable', 'integer', 'min:1', 'max:999'], 'notes' => ['nullable', 'string', 'max:200'], ]); try { $this->sales->addLine($sale, $data); } catch (RuntimeException $e) { return response()->json(['message' => $e->getMessage()], 422); } return $this->ticketJson($sale); } public function updateLine(Request $request, PosSale $sale, PosSaleLine $line): JsonResponse { $this->authorizeOpen($request, $sale); abort_unless($line->pos_sale_id === $sale->id, 404); $data = $request->validate([ 'quantity' => ['required', 'integer', 'min:0', 'max:999'], 'notes' => ['nullable', 'string', 'max:200'], ]); $this->sales->updateLine($line, $data['quantity'], $data['notes'] ?? null); return $this->ticketJson($sale); } public function removeLine(Request $request, PosSale $sale, PosSaleLine $line): JsonResponse { $this->authorizeOpen($request, $sale); abort_unless($line->pos_sale_id === $sale->id, 404); $this->sales->removeLine($line); return $this->ticketJson($sale); } public function sendToKitchen(Request $request, PosSale $sale): JsonResponse { $this->authorizeOpen($request, $sale); $fired = $this->sales->sendToKitchen($sale); $sale->refresh(); return response()->json([ 'lines' => $this->lineData($sale), 'subtotal_minor' => $sale->subtotal_minor, 'total_minor' => $sale->total_minor, 'kitchen_status' => $sale->kitchen_status, 'fired' => $fired, ]); } public function settle(Request $request, PosSale $sale): RedirectResponse { $this->authorizeOpen($request, $sale); $data = $request->validate([ 'payment_method' => ['required', 'in:pay,cash'], 'customer_name' => ['nullable', 'string', 'max:120'], 'customer_phone' => ['nullable', 'string', 'max:40'], ]); $sale->loadCount('lines'); if ($sale->lines_count === 0) { return back()->with('error', 'Add at least one item before settling.'); } if (! empty($data['customer_name']) || ! empty($data['customer_phone'])) { $sale->forceFill([ 'customer_name' => $data['customer_name'] ?? $sale->customer_name, 'customer_phone' => $data['customer_phone'] ?? $sale->customer_phone, ])->save(); } $merchant = ladill_account() ?? $request->user(); try { if ($data['payment_method'] === 'cash') { $this->sales->recordCashPayment($sale); return redirect()->route('pos.sales.show', $sale)->with('success', 'Ticket settled (cash).'); } $result = $this->sales->initiatePayCheckout($sale, $merchant); return redirect()->away($result['checkout_url']); } catch (RuntimeException $e) { return back()->with('error', $e->getMessage()); } } private function authorizeOpen(Request $request, PosSale $sale): void { $this->authorizeOwner($request, $sale); abort_unless($sale->isOpen(), 409, 'This ticket is already settled.'); } private function ticketJson(PosSale $sale): JsonResponse { $sale->refresh(); return response()->json([ 'lines' => $this->lineData($sale), 'subtotal_minor' => $sale->subtotal_minor, 'total_minor' => $sale->total_minor, 'kitchen_status' => $sale->kitchen_status, ]); } /** @return list> */ private function lineData(PosSale $sale): array { return $sale->lines()->orderBy('position')->get()->map(fn (PosSaleLine $l) => [ 'id' => $l->id, 'product_id' => $l->product_id, 'name' => $l->name, 'unit_price_minor' => $l->unit_price_minor, 'quantity' => $l->quantity, 'line_total_minor' => $l->line_total_minor, 'kitchen_state' => $l->kitchen_state, 'notes' => $l->notes, 'fired' => $l->kitchen_state !== PosSaleLine::KITCHEN_NEW, ])->all(); } }