Deploy Ladill POS / deploy (push) Successful in 31s
Send real receipt emails from the customer display and sale page, store branch promo content for the idle screen, and fold selected tips into totals.
190 lines
6.6 KiB
PHP
190 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Public;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\PosCustomerDisplay;
|
|
use App\Models\PosSale;
|
|
use App\Services\Pos\CustomerDisplayService;
|
|
use App\Services\Pos\ReceiptEmailService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
use RuntimeException;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class CustomerDisplayController extends Controller
|
|
{
|
|
public function __construct(
|
|
private CustomerDisplayService $displays,
|
|
private ReceiptEmailService $receiptEmail,
|
|
) {}
|
|
|
|
public function show(string $token): View
|
|
{
|
|
$display = $this->find($token);
|
|
|
|
return view('pos.customer-display', [
|
|
'display' => $display,
|
|
'state' => $this->displays->publicState($display),
|
|
'stateUrl' => route('pos.customer-display.state', ['token' => $token]),
|
|
'streamUrl' => route('pos.customer-display.stream', ['token' => $token]),
|
|
'actionUrl' => route('pos.customer-display.action', ['token' => $token]),
|
|
]);
|
|
}
|
|
|
|
public function state(string $token): JsonResponse
|
|
{
|
|
$display = $this->find($token);
|
|
|
|
return response()->json($this->displays->publicState($display));
|
|
}
|
|
|
|
public function stream(Request $request, string $token): StreamedResponse
|
|
{
|
|
$display = $this->find($token);
|
|
$id = $display->id;
|
|
$request->session()->save();
|
|
|
|
$response = new StreamedResponse(function () use ($id) {
|
|
while (ob_get_level() > 0) {
|
|
@ob_end_flush();
|
|
}
|
|
@set_time_limit(0);
|
|
|
|
$start = time();
|
|
$lastHash = null;
|
|
|
|
while (true) {
|
|
$display = PosCustomerDisplay::query()->find($id);
|
|
if (! $display) {
|
|
echo "event: end\ndata: {}\n\n";
|
|
break;
|
|
}
|
|
|
|
$payload = $this->displays->publicState($display);
|
|
$json = json_encode($payload);
|
|
$hash = md5((string) $json);
|
|
|
|
if ($hash !== $lastHash) {
|
|
echo 'data: '.$json."\n\n";
|
|
$lastHash = $hash;
|
|
} else {
|
|
echo ": ping\n\n";
|
|
}
|
|
@ob_flush();
|
|
@flush();
|
|
|
|
if (connection_aborted() || (time() - $start) >= 25) {
|
|
break;
|
|
}
|
|
usleep(400000);
|
|
}
|
|
});
|
|
|
|
$response->headers->set('Content-Type', 'text/event-stream');
|
|
$response->headers->set('Cache-Control', 'no-cache');
|
|
$response->headers->set('Connection', 'keep-alive');
|
|
$response->headers->set('X-Accel-Buffering', 'no');
|
|
|
|
return $response;
|
|
}
|
|
|
|
public function action(Request $request, string $token): JsonResponse
|
|
{
|
|
$display = $this->find($token);
|
|
|
|
$data = $request->validate([
|
|
'type' => ['required', 'string', 'in:tip,signature,receipt_email,payment_option'],
|
|
'tip_percent' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
|
'tip_minor' => ['nullable', 'integer', 'min:0'],
|
|
'signature_data_url' => ['nullable', 'string', 'max:500000'],
|
|
'email' => ['nullable', 'email', 'max:255'],
|
|
'payment_option' => ['nullable', 'string', 'max:40'],
|
|
]);
|
|
|
|
$action = ['type' => $data['type']];
|
|
|
|
if ($data['type'] === 'tip') {
|
|
$action['tip_percent'] = $data['tip_percent'] ?? null;
|
|
$action['tip_minor'] = $data['tip_minor'] ?? null;
|
|
// Reflect selection on the screen immediately.
|
|
$payload = $display->payload ?? [];
|
|
$tip = $payload['tip'] ?? [];
|
|
$tip['selected_percent'] = $data['tip_percent'] ?? null;
|
|
$tip['selected_minor'] = $data['tip_minor'] ?? null;
|
|
$payload['tip'] = $tip;
|
|
$display->update(['payload' => $payload, 'last_pushed_at' => now()]);
|
|
}
|
|
|
|
if ($data['type'] === 'signature') {
|
|
$action['signature_data_url'] = $data['signature_data_url'] ?? null;
|
|
$payload = $display->payload ?? [];
|
|
$sig = $payload['signature'] ?? [];
|
|
$sig['data_url'] = $data['signature_data_url'] ?? null;
|
|
$payload['signature'] = $sig;
|
|
$display->update(['payload' => $payload, 'last_pushed_at' => now()]);
|
|
}
|
|
|
|
if ($data['type'] === 'receipt_email') {
|
|
$action['email'] = $data['email'] ?? null;
|
|
$emailResult = $this->emailReceiptForDisplay($display, (string) ($data['email'] ?? ''));
|
|
$action['email_sent'] = $emailResult['ok'];
|
|
$action['email_message'] = $emailResult['message'];
|
|
}
|
|
|
|
if ($data['type'] === 'payment_option') {
|
|
$action['payment_option'] = $data['payment_option'] ?? null;
|
|
}
|
|
|
|
$this->displays->recordCustomerAction($display->fresh(), $action);
|
|
|
|
return response()->json([
|
|
'ok' => true,
|
|
'state' => $this->displays->publicState($display->fresh()),
|
|
'email_sent' => $action['email_sent'] ?? null,
|
|
'message' => $action['email_message'] ?? null,
|
|
]);
|
|
}
|
|
|
|
/** @return array{ok: bool, message: string} */
|
|
private function emailReceiptForDisplay(PosCustomerDisplay $display, string $email): array
|
|
{
|
|
$payload = is_array($display->payload) ? $display->payload : [];
|
|
$saleId = (int) ($payload['sale_id'] ?? $payload['receipt']['sale_id'] ?? 0);
|
|
$reference = (string) ($payload['sale_reference'] ?? $payload['receipt']['reference'] ?? '');
|
|
|
|
$sale = null;
|
|
if ($saleId > 0) {
|
|
$sale = PosSale::query()
|
|
->where('id', $saleId)
|
|
->where('location_id', $display->location_id)
|
|
->first();
|
|
}
|
|
if (! $sale && $reference !== '') {
|
|
$sale = PosSale::query()
|
|
->where('reference', $reference)
|
|
->where('location_id', $display->location_id)
|
|
->first();
|
|
}
|
|
|
|
if (! $sale) {
|
|
return ['ok' => false, 'message' => 'No receipt is ready to email yet.'];
|
|
}
|
|
|
|
try {
|
|
return $this->receiptEmail->send($sale, $email);
|
|
} catch (RuntimeException $e) {
|
|
return ['ok' => false, 'message' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
private function find(string $token): PosCustomerDisplay
|
|
{
|
|
return PosCustomerDisplay::query()
|
|
->where('token', $token)
|
|
->with('location')
|
|
->firstOrFail();
|
|
}
|
|
}
|