Log NetUP request/response body; fix reservation_id to be numeric
- reservation_id converted to integer (NetUP likely requires number not UUID string) - Log requestBody sent to NetUP and responseBody received from NetUP - Push event rows now expandable: click to see URL, request JSON, NetUP error response - Same for checkout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,15 +7,17 @@ type SlugIdParam = { Params: { slug: string; id: string } }
|
|||||||
|
|
||||||
// ── Push event log (check-in / check-out calls to NetUP) ─────────────────────
|
// ── Push event log (check-in / check-out calls to NetUP) ─────────────────────
|
||||||
export type PushEvent = {
|
export type PushEvent = {
|
||||||
ts: string
|
ts: string
|
||||||
action: 'check-in' | 'check-out' | 'message'
|
action: 'check-in' | 'check-out' | 'message'
|
||||||
hotelId: string
|
hotelId: string
|
||||||
roomNumber: string // PMS room number
|
roomNumber: string // PMS room number
|
||||||
netupRoom: string // NetUP room number
|
netupRoom: string // NetUP room number
|
||||||
url: string
|
url: string
|
||||||
status: 'ok' | 'error' | 'skipped'
|
requestBody?: unknown // what we sent
|
||||||
httpStatus?: number
|
status: 'ok' | 'error' | 'skipped'
|
||||||
error?: string
|
httpStatus?: number
|
||||||
|
responseBody?: string // what NetUP replied
|
||||||
|
error?: string
|
||||||
}
|
}
|
||||||
const MAX_PUSH = 100
|
const MAX_PUSH = 100
|
||||||
export const pushLog: PushEvent[] = []
|
export const pushLog: PushEvent[] = []
|
||||||
@@ -366,24 +368,31 @@ async function notifyNetupCheckinInternal(
|
|||||||
const url = `${base}/mw/api/hotel-room/${encodeURIComponent(mapping.netup_room_number)}/check-in`
|
const url = `${base}/mw/api/hotel-room/${encodeURIComponent(mapping.netup_room_number)}/check-in`
|
||||||
const cred = Buffer.from(`${cfg.username}:${cfg.password}`).toString('base64')
|
const cred = Buffer.from(`${cfg.username}:${cfg.password}`).toString('base64')
|
||||||
|
|
||||||
|
// reservation_id must be a number in NetUP — convert UUID/string to integer
|
||||||
|
const resIdNum = parseInt(reservationId.replace(/\D/g, '').slice(-8), 10) || Date.now() % 1000000
|
||||||
|
const requestBody = { reservation_id: resIdNum, name: guestName, language: language ?? cfg.default_language }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Authorization: `Basic ${cred}`, 'Content-Type': 'application/json' },
|
headers: { Authorization: `Basic ${cred}`, 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ reservation_id: reservationId, name: guestName, language: language ?? cfg.default_language }),
|
body: JSON.stringify(requestBody),
|
||||||
signal: AbortSignal.timeout(6000),
|
signal: AbortSignal.timeout(6000),
|
||||||
})
|
})
|
||||||
|
const responseText = await res.text().catch(() => '')
|
||||||
const event: PushEvent = {
|
const event: PushEvent = {
|
||||||
ts: new Date().toISOString(), action: 'check-in', hotelId,
|
ts: new Date().toISOString(), action: 'check-in', hotelId,
|
||||||
roomNumber, netupRoom: mapping.netup_room_number, url,
|
roomNumber, netupRoom: mapping.netup_room_number, url,
|
||||||
|
requestBody,
|
||||||
status: res.ok ? 'ok' : 'error', httpStatus: res.status,
|
status: res.ok ? 'ok' : 'error', httpStatus: res.status,
|
||||||
|
responseBody: responseText || undefined,
|
||||||
}
|
}
|
||||||
if (!res.ok) event.error = `HTTP ${res.status}`
|
if (!res.ok) event.error = `HTTP ${res.status}`
|
||||||
recordPush(event)
|
recordPush(event)
|
||||||
return res.ok ? { ok: true, netupRoom: mapping.netup_room_number } : { ok: false, error: `HTTP ${res.status}` }
|
return res.ok ? { ok: true, netupRoom: mapping.netup_room_number } : { ok: false, error: `HTTP ${res.status}: ${responseText}` }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err instanceof Error ? err.message : 'Ошибка соединения'
|
const error = err instanceof Error ? err.message : 'Ошибка соединения'
|
||||||
recordPush({ ts: new Date().toISOString(), action: 'check-in', hotelId, roomNumber, netupRoom: mapping.netup_room_number, url, status: 'error', error })
|
recordPush({ ts: new Date().toISOString(), action: 'check-in', hotelId, roomNumber, netupRoom: mapping.netup_room_number, url, requestBody, status: 'error', error })
|
||||||
return { ok: false, error }
|
return { ok: false, error }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,10 +428,12 @@ export async function notifyNetupCheckout(hotelId: string, roomId: string) {
|
|||||||
headers: { Authorization: `Basic ${cred}` },
|
headers: { Authorization: `Basic ${cred}` },
|
||||||
signal: AbortSignal.timeout(6000),
|
signal: AbortSignal.timeout(6000),
|
||||||
})
|
})
|
||||||
|
const responseText = await res.text().catch(() => '')
|
||||||
recordPush({
|
recordPush({
|
||||||
ts: new Date().toISOString(), action: 'check-out', hotelId,
|
ts: new Date().toISOString(), action: 'check-out', hotelId,
|
||||||
roomNumber, netupRoom: mapping.netup_room_number, url,
|
roomNumber, netupRoom: mapping.netup_room_number, url,
|
||||||
status: res.ok ? 'ok' : 'error', httpStatus: res.status,
|
status: res.ok ? 'ok' : 'error', httpStatus: res.status,
|
||||||
|
responseBody: responseText || undefined,
|
||||||
error: res.ok ? undefined : `HTTP ${res.status}`,
|
error: res.ok ? undefined : `HTTP ${res.status}`,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ export const api = {
|
|||||||
|
|
||||||
getLog: (slug: string) =>
|
getLog: (slug: string) =>
|
||||||
req<{
|
req<{
|
||||||
pushEvents: { ts: string; action: string; roomNumber: string; netupRoom: string; url: string; status: 'ok' | 'error' | 'skipped'; httpStatus?: number; error?: string }[]
|
pushEvents: { ts: string; action: string; roomNumber: string; netupRoom: string; url: string; requestBody?: unknown; status: 'ok' | 'error' | 'skipped'; httpStatus?: number; responseBody?: string; error?: string }[]
|
||||||
pullRequests: { ts: string; method: string; url: string; headers: Record<string, unknown>; query: Record<string, unknown>; body: unknown }[]
|
pullRequests: { ts: string; method: string; url: string; headers: Record<string, unknown>; query: Record<string, unknown>; body: unknown }[]
|
||||||
}>('GET', `/api/hotels/${slug}/netup/log`),
|
}>('GET', `/api/hotels/${slug}/netup/log`),
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ type PushEvent = {
|
|||||||
roomNumber: string
|
roomNumber: string
|
||||||
netupRoom: string
|
netupRoom: string
|
||||||
url: string
|
url: string
|
||||||
|
requestBody?: unknown
|
||||||
status: 'ok' | 'error' | 'skipped'
|
status: 'ok' | 'error' | 'skipped'
|
||||||
httpStatus?: number
|
httpStatus?: number
|
||||||
|
responseBody?: string
|
||||||
error?: string
|
error?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +67,8 @@ export function TvWelcomePage() {
|
|||||||
const [pushEvents, setPushEvents] = useState<PushEvent[]>([])
|
const [pushEvents, setPushEvents] = useState<PushEvent[]>([])
|
||||||
const [pullEntries, setPullEntries] = useState<PullEntry[]>([])
|
const [pullEntries, setPullEntries] = useState<PullEntry[]>([])
|
||||||
const [logLoading, setLogLoading] = useState(false)
|
const [logLoading, setLogLoading] = useState(false)
|
||||||
const [selectedPull, setSelectedPull] = useState<PullEntry | null>(null)
|
const [selectedPull, setSelectedPull] = useState<PullEntry | null>(null)
|
||||||
|
const [selectedPush, setSelectedPush] = useState<PushEvent | null>(null)
|
||||||
// Test check-in
|
// Test check-in
|
||||||
const [testingCheckin, setTestingCheckin] = useState(false)
|
const [testingCheckin, setTestingCheckin] = useState(false)
|
||||||
const [testCheckinResult, setTestCheckinResult] = useState<{ ok: boolean; msg: string } | null>(null)
|
const [testCheckinResult, setTestCheckinResult] = useState<{ ok: boolean; msg: string } | null>(null)
|
||||||
@@ -548,23 +551,53 @@ export function TvWelcomePage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-slate-100 dark:divide-slate-700/50">
|
<div className="divide-y divide-slate-100 dark:divide-slate-700/50">
|
||||||
{pushEvents.map((e, i) => (
|
{pushEvents.map((e, i) => (
|
||||||
<div key={i} className="flex items-center gap-3 py-2.5 px-1 text-sm">
|
<div key={i}>
|
||||||
<span className={cn(
|
<button
|
||||||
'w-2 h-2 rounded-full shrink-0',
|
onClick={() => setSelectedPush(selectedPush === e ? null : e)}
|
||||||
e.status === 'ok' ? 'bg-emerald-500' : e.status === 'skipped' ? 'bg-amber-400' : 'bg-red-500',
|
className={cn(
|
||||||
)} />
|
'w-full flex items-center gap-3 py-2.5 px-1 text-sm rounded-lg transition-colors',
|
||||||
<span className="font-medium text-slate-700 dark:text-slate-200 w-20 shrink-0">
|
selectedPush === e ? 'bg-slate-50 dark:bg-slate-700/40' : 'hover:bg-slate-50/60 dark:hover:bg-slate-700/20',
|
||||||
{e.action === 'check-in' ? 'Заселение' : 'Выезд'}
|
)}
|
||||||
</span>
|
>
|
||||||
<span className="text-slate-500 dark:text-slate-400">
|
<span className={cn(
|
||||||
Номер <span className="font-medium text-slate-700 dark:text-slate-200">{e.roomNumber}</span>
|
'w-2 h-2 rounded-full shrink-0',
|
||||||
{e.netupRoom && <> → NetUP <span className="font-mono text-xs">{e.netupRoom}</span></>}
|
e.status === 'ok' ? 'bg-emerald-500' : e.status === 'skipped' ? 'bg-amber-400' : 'bg-red-500',
|
||||||
</span>
|
)} />
|
||||||
{e.error && <span className="text-red-500 dark:text-red-400 text-xs flex-1 truncate">{e.error}</span>}
|
<span className="font-medium text-slate-700 dark:text-slate-200 w-20 shrink-0">
|
||||||
{e.httpStatus && e.status === 'ok' && <span className="text-xs text-emerald-600 dark:text-emerald-400">HTTP {e.httpStatus}</span>}
|
{e.action === 'check-in' ? 'Заселение' : 'Выезд'}
|
||||||
<span className="ml-auto text-xs text-slate-400 shrink-0">
|
</span>
|
||||||
{new Date(e.ts).toLocaleTimeString('ru-RU')}
|
<span className="text-slate-500 dark:text-slate-400">
|
||||||
</span>
|
Номер <span className="font-medium text-slate-700 dark:text-slate-200">{e.roomNumber}</span>
|
||||||
|
{e.netupRoom && <> → NetUP <span className="font-mono text-xs">{e.netupRoom}</span></>}
|
||||||
|
</span>
|
||||||
|
{e.error && <span className="text-red-500 dark:text-red-400 text-xs flex-1 truncate text-left">{e.error}</span>}
|
||||||
|
{e.httpStatus && e.status === 'ok' && <span className="text-xs text-emerald-600 dark:text-emerald-400">HTTP {e.httpStatus}</span>}
|
||||||
|
<span className="ml-auto text-xs text-slate-400 shrink-0">
|
||||||
|
{new Date(e.ts).toLocaleTimeString('ru-RU')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{selectedPush === e && (
|
||||||
|
<div className="mx-1 mb-2 bg-slate-900 dark:bg-slate-950 rounded-xl p-4 space-y-3 text-xs font-mono">
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 font-sans font-semibold mb-1">URL запроса</p>
|
||||||
|
<p className="text-violet-400 break-all">{e.url}</p>
|
||||||
|
</div>
|
||||||
|
{e.requestBody != null && (
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 font-sans font-semibold mb-1">Тело запроса (отправили в NetUP)</p>
|
||||||
|
<pre className="text-emerald-300 whitespace-pre-wrap break-all">
|
||||||
|
{JSON.stringify(e.requestBody, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{e.responseBody && (
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-400 font-sans font-semibold mb-1">Ответ NetUP</p>
|
||||||
|
<pre className="text-red-300 whitespace-pre-wrap break-all">{e.responseBody}</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user