Deploy Ladill POS / deploy (push) Successful in 36s
Split bills (restaurant tickets): - New pos_payments ledger; a tab is settled across one or more cash/Ladill Pay payments. The ticket shows total / paid / balance, an amount field with Full / ½ / ⅓ / ¼ helpers, and the payments taken. Partial payments keep the tab open; the sale finalises (and frees the table) only when the balance hits zero. Pay splits get their own checkout + callback (pos.payments.callback). Pipe online orders to the KDS: - POST /api/kitchen/orders — a first-party, service-keyed ingest (config pos.kitchen_api_keys, scoped by owner, idempotent by external_ref) that creates a paid, already-fired ticket (order_type=online, lines source=online). - The KDS feed is now payment-agnostic (any kitchen-active sale), so paid online orders sit on the board next to dine-in tabs and bump the same way; they're badged "online". Schema additive: pos_payments, pos_sales.external_ref. Suite green (14). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
60 lines
2.1 KiB
PHP
60 lines
2.1 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Services\Pos\PosSaleService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* First-party kitchen ingest — lets another Ladill app (e.g. Merchant) push a
|
|
* paid online/QR order onto this account's Kitchen Display. Authenticated with a
|
|
* per-service key (Authorization: Bearer <KITCHEN_API_KEY_*>) and scoped to an
|
|
* account by the `owner` parameter. Idempotent by `reference`.
|
|
*/
|
|
class KitchenIngestController extends Controller
|
|
{
|
|
public function __construct(private PosSaleService $sales) {}
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
abort_unless($this->service($request) !== null, 401, 'Invalid service key.');
|
|
|
|
$data = $request->validate([
|
|
'owner' => ['required', 'string', 'max:255'],
|
|
'reference' => ['required', 'string', 'max:255'],
|
|
'customer_name' => ['nullable', 'string', 'max:120'],
|
|
'items' => ['required', 'array', 'min:1', 'max:100'],
|
|
'items.*.name' => ['required', 'string', 'max:200'],
|
|
'items.*.quantity' => ['nullable', 'integer', 'min:1', 'max:200'],
|
|
'items.*.unit_price_minor' => ['nullable', 'integer', 'min:0'],
|
|
'items.*.notes' => ['nullable', 'string', 'max:200'],
|
|
]);
|
|
|
|
try {
|
|
$sale = $this->sales->ingestExternalOrder($data);
|
|
} catch (RuntimeException $e) {
|
|
return response()->json(['message' => $e->getMessage()], 422);
|
|
}
|
|
|
|
return response()->json(['id' => $sale->id, 'reference' => $sale->reference], 201);
|
|
}
|
|
|
|
private function service(Request $request): ?string
|
|
{
|
|
$token = (string) $request->bearerToken();
|
|
if ($token === '') {
|
|
return null;
|
|
}
|
|
foreach ((array) config('pos.kitchen_api_keys', []) as $service => $key) {
|
|
if ((string) $key !== '' && hash_equals((string) $key, $token)) {
|
|
return (string) $service;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|