diff --git a/api/guest_action_queue.php b/api/guest_action_queue.php
index 861ec95..2de423b 100644
--- a/api/guest_action_queue.php
+++ b/api/guest_action_queue.php
@@ -47,6 +47,29 @@ function hasPendingGuestAction(PDO $pdo, string $tableId, string $messageType):
return (bool) $stmt->fetchColumn();
}
+/**
+ * Limit: max 1 nowe zgłoszenie danego typu na stolik w oknie czasowym
+ * (nawet po obsłużeniu poprzedniego w KDS).
+ */
+function isGuestActionRateLimited(PDO $pdo, string $tableId, string $messageType, int $windowSeconds = 60): bool
+{
+ $windowSeconds = max(1, min(600, $windowSeconds));
+ $stmt = $pdo->prepare("
+ SELECT 1
+ FROM guest_action_queue
+ WHERE table_id = :table_id
+ AND message_type = :message_type
+ AND created_at >= (NOW(3) - INTERVAL {$windowSeconds} SECOND)
+ LIMIT 1
+ ");
+ $stmt->execute([
+ ':table_id' => $tableId,
+ ':message_type' => $messageType,
+ ]);
+
+ return (bool) $stmt->fetchColumn();
+}
+
function fetchPendingGuestActions(PDO $pdo, string $tableId): array
{
$stmt = $pdo->prepare("
@@ -316,6 +339,18 @@ try {
exit;
}
+ if (isGuestActionRateLimited($pdo, $tableId, $messageType, 60)) {
+ http_response_code(429);
+ echo json_encode([
+ 'status' => 'error',
+ 'code' => 'rate_limited',
+ 'message' => $messageType === 'waiter_call'
+ ? 'Wezwanie zostało już niedawno wysłane. Spróbuj ponownie za chwilę.'
+ : 'Prośba o rachunek została już niedawno wysłana. Spróbuj ponownie za chwilę.',
+ ], JSON_UNESCAPED_UNICODE);
+ exit;
+ }
+
$stmt = $pdo->prepare("
INSERT INTO guest_action_queue (
table_id,
diff --git a/api/gus_lookup.php b/api/gus_lookup.php
new file mode 100644
index 0000000..4fa5eeb
--- /dev/null
+++ b/api/gus_lookup.php
@@ -0,0 +1,116 @@
+ 'error',
+ 'message' => 'Method not allowed',
+ ], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+$nipRaw = '';
+if ($_SERVER['REQUEST_METHOD'] === 'GET') {
+ $nipRaw = isset($_GET['nip']) ? (string) $_GET['nip'] : '';
+} else {
+ $rawBody = file_get_contents('php://input');
+ $data = json_decode($rawBody ?: '', true);
+ if (is_array($data) && isset($data['nip'])) {
+ $nipRaw = (string) $data['nip'];
+ } elseif (isset($_POST['nip'])) {
+ $nipRaw = (string) $_POST['nip'];
+ }
+}
+
+$nip = preg_replace('/[\s\-]/', '', $nipRaw) ?? '';
+
+if ($nip === '' || !preg_match('/^\d{10}$/', $nip)) {
+ http_response_code(422);
+ echo json_encode([
+ 'status' => false,
+ 'message' => 'Podaj poprawny 10-cyfrowy NIP.',
+ ], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+$config = require __DIR__ . '/../config/gus.php';
+$apiKey = trim((string) ($config['api_key'] ?? ''));
+$baseUrl = rtrim((string) ($config['base_url'] ?? ''), '/');
+$timeout = (int) ($config['timeout_seconds'] ?? 12);
+
+if ($apiKey === '' || $baseUrl === '') {
+ http_response_code(500);
+ echo json_encode([
+ 'status' => false,
+ 'message' => 'Brak konfiguracji GUS.',
+ ], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+$url = $baseUrl . '/' . rawurlencode($nip) . '?api_key=' . rawurlencode($apiKey);
+
+$responseBody = false;
+$httpCode = 0;
+
+if (function_exists('curl_init')) {
+ $ch = curl_init($url);
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => max(3, $timeout),
+ CURLOPT_HTTPHEADER => [
+ 'Accept: application/json',
+ ],
+ ]);
+
+ $responseBody = curl_exec($ch);
+ $curlErrno = curl_errno($ch);
+ $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+
+ if ($curlErrno !== 0) {
+ $responseBody = false;
+ }
+} else {
+ $context = stream_context_create([
+ 'http' => [
+ 'method' => 'GET',
+ 'timeout' => max(3, $timeout),
+ 'header' => "Accept: application/json\r\n",
+ 'ignore_errors' => true,
+ ],
+ ]);
+ $responseBody = @file_get_contents($url, false, $context);
+ if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
+ $httpCode = (int) $m[1];
+ }
+}
+
+if ($responseBody === false) {
+ http_response_code(502);
+ echo json_encode([
+ 'status' => false,
+ 'message' => 'Błąd połączenia z API GUS.',
+ ], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+$decoded = json_decode($responseBody, true);
+if (!is_array($decoded)) {
+ http_response_code(502);
+ echo json_encode([
+ 'status' => false,
+ 'message' => 'Niepoprawna odpowiedź API GUS.',
+ ], JSON_UNESCAPED_UNICODE);
+ exit;
+}
+
+if ($httpCode < 200 || $httpCode >= 300) {
+ http_response_code($httpCode >= 400 ? $httpCode : 502);
+}
+
+echo json_encode($decoded, JSON_UNESCAPED_UNICODE);
diff --git a/api/request_ip.php b/api/request_ip.php
index d76f5a6..c67daf9 100644
--- a/api/request_ip.php
+++ b/api/request_ip.php
@@ -14,6 +14,19 @@ function getRequestClientIp(): string
return trim((string) ($_SERVER['REMOTE_ADDR'] ?? ''));
}
+/**
+ * Hostnamey, dla których frontend pomija geolokalizację (dev / znany host).
+ * Wstrzykiwane do window.APP_CONFIG przez app.php.
+ */
+function getGeoBypassTrustedHosts(): array
+{
+ return [
+ '82.160.190.247',
+ 'localhost',
+ '127.0.0.1',
+ ];
+}
+
/**
* Pojedyncze IP z pominięciem geo (zewnętrzne, dev, przykładowe hosty LAN).
*/
diff --git a/config/gus.php b/config/gus.php
new file mode 100644
index 0000000..6fbc93e
--- /dev/null
+++ b/config/gus.php
@@ -0,0 +1,12 @@
+ 'd8bdc252-e0f1-4863-97a1-826faddbc49c',
+ 'base_url' => 'https://api.magico.pro/v1/gus',
+ 'timeout_seconds' => 12,
+];
diff --git a/public/app.php b/public/app.php
index 99fdb01..42669e7 100644
--- a/public/app.php
+++ b/public/app.php
@@ -5,9 +5,10 @@ header('Pragma: no-cache');
header('Expires: 0');
require_once __DIR__ . '/includes/asset_version.php';
+require_once __DIR__ . '/../api/request_ip.php';
$publicDir = __DIR__;
$vCss = publicAssetVersion($publicDir, 'assets/css/app.css');
-$vJs = publicAssetVersion($publicDir, 'assets/js/app.js');
+$vJs = publicJsBundleVersion($publicDir, 'assets/js/app.js');
$vMenu = publicAssetVersion($publicDir, 'menu.json');
?>
@@ -383,8 +384,20 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
-
+