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) ─────────────────────
|
||||
export type PushEvent = {
|
||||
ts: string
|
||||
action: 'check-in' | 'check-out' | 'message'
|
||||
hotelId: string
|
||||
roomNumber: string // PMS room number
|
||||
netupRoom: string // NetUP room number
|
||||
url: string
|
||||
status: 'ok' | 'error' | 'skipped'
|
||||
httpStatus?: number
|
||||
error?: string
|
||||
ts: string
|
||||
action: 'check-in' | 'check-out' | 'message'
|
||||
hotelId: string
|
||||
roomNumber: string // PMS room number
|
||||
netupRoom: string // NetUP room number
|
||||
url: string
|
||||
requestBody?: unknown // what we sent
|
||||
status: 'ok' | 'error' | 'skipped'
|
||||
httpStatus?: number
|
||||
responseBody?: string // what NetUP replied
|
||||
error?: string
|
||||
}
|
||||
const MAX_PUSH = 100
|
||||
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 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 {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
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),
|
||||
})
|
||||
const responseText = await res.text().catch(() => '')
|
||||
const event: PushEvent = {
|
||||
ts: new Date().toISOString(), action: 'check-in', hotelId,
|
||||
roomNumber, netupRoom: mapping.netup_room_number, url,
|
||||
requestBody,
|
||||
status: res.ok ? 'ok' : 'error', httpStatus: res.status,
|
||||
responseBody: responseText || undefined,
|
||||
}
|
||||
if (!res.ok) event.error = `HTTP ${res.status}`
|
||||
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) {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -419,10 +428,12 @@ export async function notifyNetupCheckout(hotelId: string, roomId: string) {
|
||||
headers: { Authorization: `Basic ${cred}` },
|
||||
signal: AbortSignal.timeout(6000),
|
||||
})
|
||||
const responseText = await res.text().catch(() => '')
|
||||
recordPush({
|
||||
ts: new Date().toISOString(), action: 'check-out', hotelId,
|
||||
roomNumber, netupRoom: mapping.netup_room_number, url,
|
||||
status: res.ok ? 'ok' : 'error', httpStatus: res.status,
|
||||
responseBody: responseText || undefined,
|
||||
error: res.ok ? undefined : `HTTP ${res.status}`,
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
@@ -290,7 +290,7 @@ export const api = {
|
||||
|
||||
getLog: (slug: string) =>
|
||||
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 }[]
|
||||
}>('GET', `/api/hotels/${slug}/netup/log`),
|
||||
|
||||
|
||||
@@ -31,8 +31,10 @@ type PushEvent = {
|
||||
roomNumber: string
|
||||
netupRoom: string
|
||||
url: string
|
||||
requestBody?: unknown
|
||||
status: 'ok' | 'error' | 'skipped'
|
||||
httpStatus?: number
|
||||
responseBody?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
@@ -65,7 +67,8 @@ export function TvWelcomePage() {
|
||||
const [pushEvents, setPushEvents] = useState<PushEvent[]>([])
|
||||
const [pullEntries, setPullEntries] = useState<PullEntry[]>([])
|
||||
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
|
||||
const [testingCheckin, setTestingCheckin] = useState(false)
|
||||
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">
|
||||
{pushEvents.map((e, i) => (
|
||||
<div key={i} className="flex items-center gap-3 py-2.5 px-1 text-sm">
|
||||
<span className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
e.status === 'ok' ? 'bg-emerald-500' : e.status === 'skipped' ? 'bg-amber-400' : 'bg-red-500',
|
||||
)} />
|
||||
<span className="font-medium text-slate-700 dark:text-slate-200 w-20 shrink-0">
|
||||
{e.action === 'check-in' ? 'Заселение' : 'Выезд'}
|
||||
</span>
|
||||
<span className="text-slate-500 dark:text-slate-400">
|
||||
Номер <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">{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>
|
||||
<div key={i}>
|
||||
<button
|
||||
onClick={() => setSelectedPush(selectedPush === e ? null : e)}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 py-2.5 px-1 text-sm rounded-lg transition-colors',
|
||||
selectedPush === e ? 'bg-slate-50 dark:bg-slate-700/40' : 'hover:bg-slate-50/60 dark:hover:bg-slate-700/20',
|
||||
)}
|
||||
>
|
||||
<span className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
e.status === 'ok' ? 'bg-emerald-500' : e.status === 'skipped' ? 'bg-amber-400' : 'bg-red-500',
|
||||
)} />
|
||||
<span className="font-medium text-slate-700 dark:text-slate-200 w-20 shrink-0">
|
||||
{e.action === 'check-in' ? 'Заселение' : 'Выезд'}
|
||||
</span>
|
||||
<span className="text-slate-500 dark:text-slate-400">
|
||||
Номер <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>
|
||||
|
||||
Reference in New Issue
Block a user