lib/friends-store.mjs:2:// No Cloudflare-specific code: swap the adapter (D1, better-sqlite3, pg via $n rewrite) to move to the Holitech server.
lib/friends-store.mjs:11:    ensure: (tq, h, now) => A.run('INSERT INTO fr_profile(tq,h,updated_at) VALUES(?,?,?) ON CONFLICT(tq) DO NOTHING', [tq, h, now]),
lib/friends-store.mjs:18:    setStay: (tq, scope, stay, now, exp) => A.run('INSERT INTO fr_stay(tq,scope,stay,consent_at,expires_at) VALUES(?,?,?,?,?) ON CONFLICT(tq) DO UPDATE SET scope=excluded.scope, stay=excluded.stay, consent_at=excluded.consent_at, expires_at=excluded.expires_at', [tq, scope, stay, now, exp]),
lib/friends-store.mjs:27:    addEdge: (src, dst, kind, now) => A.run('INSERT INTO fr_edge(src,dst,kind,created_at) VALUES(?,?,?,?) ON CONFLICT DO NOTHING', [src, dst, kind, now]),
lib/friends-store.mjs:30:    hit: async (k, win, max) => { await A.run('INSERT INTO fr_rate(k,win,n) VALUES(?,?,1) ON CONFLICT(k) DO UPDATE SET n=CASE WHEN fr_rate.win=excluded.win THEN fr_rate.n+1 ELSE 1 END, win=excluded.win', [k, win]); const r = await one('SELECT n FROM fr_rate WHERE k=?', [k]); return r && r.n > max; },
lib/phieu-sign.mjs:10:  await db.batch([
lib/phieu-sign.mjs:13:    db.prepare("CREATE TRIGGER IF NOT EXISTS hotel_phieu_signed_no_update BEFORE UPDATE ON hotel_phieu_signed BEGIN SELECT RAISE(ABORT, 'append-only'); END"),
lib/phieu-sign.mjs:14:    db.prepare("CREATE TRIGGER IF NOT EXISTS hotel_phieu_signed_no_delete BEFORE DELETE ON hotel_phieu_signed BEGIN SELECT RAISE(ABORT, 'append-only'); END"),
lib/phieu-sign.mjs:47:  if (db && typeof db.prepare === 'function') { await table(db); return await db.prepare('SELECT * FROM hotel_phieu_signed WHERE hid = ? AND bk = ? ORDER BY at DESC LIMIT 1').bind(hid, bk).first(); }
lib/test-synthetic.mjs:14:  try { const r=await env.HOTEL_STATE.prepare('SELECT id FROM hotel_test_isolation WHERE id = ?').bind(SYNTHETIC_DB_ID).first();return r?.id===SYNTHETIC_DB_ID; }catch{return false}
lib/call-sig.mjs:126:  return (await d.prepare('SELECT * FROM r3_call_sig WHERE id = ?').bind(id).first()) || null;
lib/call-sig.mjs:132:  await d.prepare('INSERT OR REPLACE INTO r3_call_sig (id, hid, room, bk, st, offer, answer, staff, at, upd) VALUES (?,?,?,?,?,?,?,?,?,?)')
lib/app-guest.mjs:8:// r11t: Kết bạn thuộc nền tảng TravelQ ID (D1 riêng FRIENDS_DB = travelq-friends, không đụng HOTEL_STATE); khách sạn không quản lý. Danh mục, giá, nội quy, Qee, thông báo: lib/order-app.mjs (Hotel Admin › Quản lý ứng dụng Order).
lib/app-guest.mjs:98:    const k = b.key, ci = s.checkedIn[k], adults = +((/(\d+)\s*NL/.exec(b.guests) || [0, 1])[1]);
lib/app-guest.mjs:280:  if (seg[0] === 'friends') { /* Kết bạn: nền tảng TravelQ, DB riêng FRIENDS_DB; khách sạn không tham gia (stay token chỉ để mở /order/) */
lib/app-guest.mjs:281:    if (!env.FRIENDS_DB) return E(404, 'friends_off', 'Tính năng tìm bạn sẽ mở sớm.');
lib/app-guest.mjs:287:    const [st, o] = await friendsApi(d1Adapter(env.FRIENDS_DB), ctx, seg.slice(1).join('/'), M, url.searchParams, b);
lib/app-guest.mjs:463:      let mx = S.seq || 0; for (const o of S.orders) { const m = /^OD-(\d+)$/.exec(o.id); if (m) mx = Math.max(mx, +m[1]); }
lib/app-guest.mjs:468:        const m = /^OD-(\d+)$/.exec(o.id); if (m) mx = Math.max(mx, +m[1]);
lib/hotel-staff.mjs:20:export async function staffRecord(env, hid, sid) { if (!env.HOTEL_STATE) return null; return (await db(env)).prepare('SELECT * FROM hotel_staff WHERE hid=? AND sid=?').bind(hid, sid).first(); }
lib/hotel-staff.mjs:27:  const d = await db(env); return !!(await d.prepare('SELECT h FROM hotel_token_revoke WHERE h=?').bind(h).first());
lib/hotel-staff.mjs:31:  const d = await db(env); await d.prepare('INSERT OR REPLACE INTO hotel_token_revoke (h,at,hid,sid) VALUES (?,?,?,?)').bind(h, Date.now(), hid || '', sid || '').run();
lib/tqid-proxy.mjs:76:  const ac = new AbortController(); const t = setTimeout(() => ac.abort(), 9000);
lib/export-files.mjs:88:  return { meta, header, data, aoa: [...meta, header, ...data] };
lib/export-files.mjs:123:  return { meta, header, data, aoa: [...meta, header, ...data], empty: safe.length === 0 };
lib/khai-bao.mjs:16:const adultsOf = (b) => +((/(\d+)\s*NL/.exec((b && b.guests) || '') || [0, 1])[1]);
lib/khai-bao.mjs:17:const kidsOf = (b) => { const n = +((/(\d+)\s*TE/.exec((b && b.guests) || '') || [0, 0])[1]); const p = ((b && b.party) || []).filter((g) => g.type === 'child' || (g.type === 'pending' && g.kind === 'te')); return Math.max(n, p.length); };
lib/khai-bao.mjs:36:  const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(String(dob || '')); if (!m) return null;
lib/order-remind.mjs:101:    target: meta.key,
lib/order-remind.mjs:102:    label: meta.label,
lib/order-remind.mjs:103:    receiverRole: meta.receiverRole,
lib/legal-draft.mjs:19: const dobYmd=v=>{const m=/^(\d{2})\/(\d{2})\/(\d{4})$/.exec(v);return m?m[3]+'-'+m[2]+'-'+m[1]:null;};
lib/order-cid-claim.mjs:41:  /* Sequential (not batch): node:sqlite validates INDEX at prepare-time before TABLE exists. D1 accepts both. */
lib/order-cid-claim.mjs:73:  if (bound.first) return (await bound.first()) || null;
lib/order-cid-claim.mjs:130:    const ch = res && (res.meta ? res.meta.changes : res.changes);
lib/order-cid-claim.mjs:177:  const ch = res && (res.meta ? res.meta.changes : res.changes);
lib/order-cid-claim.mjs:241:  const ch = res && (res.meta ? res.meta.changes : res.changes);
lib/order-cid-claim.mjs:257:    const m = /^OD-(\d+)$/.exec(o.id);
lib/room-chat.mjs:84:  const row = await d.prepare('SELECT last_read_at FROM hotel_room_chat_read WHERE hid = ? AND bk = ? AND role = ?').bind(hid, bk, role).first();
lib/room-chat.mjs:101:  await d.prepare('INSERT INTO hotel_room_chat_read (hid, bk, role, last_read_at) VALUES (?,?,?,?) ON CONFLICT(hid,bk,role) DO UPDATE SET last_read_at = excluded.last_read_at').bind(hid, bk, role, n).run();
lib/room-chat.mjs:114:  const row = await d.prepare('SELECT COUNT(*) AS n FROM hotel_room_chat WHERE hid = ? AND bk = ? AND role = ? AND at > ?').bind(hid, bk, other, last).first();
lib/room-chat.mjs:141:      const prev = await d.prepare('SELECT * FROM hotel_room_chat WHERE hid = ? AND bk = ? AND cid = ?').bind(hid, bk, cid).first();
lib/pay-sim.mjs:146: * Implementers replace sim with adapter that returns paid_verified after bank webhook.
lib/ht-store.mjs:14:  await db.batch([
lib/ht-store.mjs:18:    db.prepare("CREATE TRIGGER IF NOT EXISTS ht_audit_no_update BEFORE UPDATE ON ht_audit BEGIN SELECT RAISE(ABORT, 'ht_audit is append-only'); END"),
lib/ht-store.mjs:19:    db.prepare("CREATE TRIGGER IF NOT EXISTS ht_audit_no_delete BEFORE DELETE ON ht_audit BEGIN SELECT RAISE(ABORT, 'ht_audit is append-only'); END"),
lib/ht-store.mjs:26:  if (db) { await init(db); const r = await db.prepare('SELECT v FROM ht_state WHERE k = ?').bind(k).first(); if (r) return JSON.parse(r.v); const s = initial(); await saveHt(env, s); return s; }
lib/ht-store.mjs:32:  if (db) { await init(db); await db.prepare('INSERT OR REPLACE INTO ht_state (k, v, at) VALUES (?, ?, ?)').bind(k, JSON.stringify(s), Date.now()).run(); }
lib/ht-store.mjs:43:    const last = await db.prepare('SELECT hash FROM ht_audit WHERE sk = ? ORDER BY seq DESC LIMIT 1').bind(k).first();
lib/domain/derive.mjs:16:    guestCount += party.length || (+((/(\d+)\s*NL/.exec(b.guests || '') || [0, 0])[1]) + +((/(\d+)\s*TE/.exec(b.guests || '') || [0, 0])[1])) || 0;
lib/order-app.mjs:43:    const row = await db.prepare('SELECT v FROM hotel_state WHERE k = ?').bind(k).first();
lib/order-app.mjs:50:  if (db) { if (!ready) { await db.prepare('CREATE TABLE IF NOT EXISTS hotel_state (k TEXT PRIMARY KEY, v TEXT NOT NULL, at INTEGER NOT NULL)').run(); ready = true; } await db.prepare('INSERT OR REPLACE INTO hotel_state (k, v, at) VALUES (?, ?, ?)').bind(k, v, Date.now()).run(); }
lib/domain/assign-flow.mjs:187:    by: meta.by || null,
lib/he-thong.mjs:132:  if (r.type === 'topup') return u.role === 'ca-nuoc' || (u.role === 'cap-tinh' && +r.data.qty <= settingsOf(s, env).topupProvinceMax);
lib/he-thong.mjs:212:const reqOut = (s, u, r, env) => { const h = s.hotels[r.hotel] || {}; const by = s.users[r.by]; const data = r.type === 'fee' && !canFees(u) ? { hidden: true } : r.type === 'price' && !canPrices(u) ? { kind: r.data.kind, label: r.data.label, cls: r.data.cls, hidden: true } : r.type === 'oprice' && !canPrices(u) ? { item: r.data.item, label: r.data.label, hidden: true } : r.data; const cls = h.classes && r.data && r.data.cls ? h.classes.find((c) => c.cls === r.data.cls) : null; const cap = r.type === 'topup' ? (cls ? cls.rooms : totalRooms(h)) : null; const hvNow = r.type === 'topup' ? Math.round((cap || 0) * settingsOf(s, env).hvMaxSharePct / 100) : null; return { ...r, data, hotelName: h.name, province: h.province, provinceName: PROVINCES[h.province] || h.province, salesName: (s.users[h.sales] || {}).name || h.sales || null, byName: by ? by.name : r.by, doneByName: r.doneBy ? (s.users[r.doneBy] || {}).name || r.doneBy : null, canDecide: canDecide(u, r, h, s, env), needs: needText(r, s, env), hvCap: hvNow, hvPct: r.type === 'topup' ? settingsOf(s, env).hvMaxSharePct : null, roomsTotal: r.type === 'topup' ? (cap || totalRooms(h)) : null }; };
lib/he-thong.mjs:214:  if (r.type === 'topup') return +r.data.qty <= settingsOf(s, env).topupProvinceMax ? 'Quản trị tỉnh hoặc HQ duyệt' : 'Vượt ' + settingsOf(s, env).topupProvinceMax + ' phòng · HQ duyệt';
lib/he-thong.mjs:255:    topups: s.reqs.filter((r) => r.type === 'topup' && r.hotel === hid).slice(0, 20).map((r) => ({ id: r.id, at: r.at, cls: r.data.cls, date: r.data.date, qty: r.data.qty, status: r.status, by: r.by })),
lib/he-thong.mjs:256:    reduces: s.reqs.filter((r) => r.type === 'hv-reduce' && r.hotel === hid).slice(0, 30).map((r) => ({ id: r.id, at: r.at, cls: r.data.cls, date: r.data.date, fromQty: r.data.fromQty, toQty: r.data.toQty, mode: r.data.mode, status: r.status, by: r.by, reason: r.reason || null, doneAt: r.doneAt || null, label: r.status === 'duyet' ? 'Khách sạn chủ động giảm số lượng' : r.status === 'tu-choi' ? 'Từ chối giảm' : 'Chờ TravelQ duyệt' })),
lib/he-thong.mjs:271:    const pend = s.reqs.find((r) => r.type === 'hv-reduce' && r.hotel === hid && r.status === 'cho' && r.data.cls === c.cls && r.data.date === date);
lib/he-thong.mjs:275:    await audit(env, { actor, role: staff.role, action: 'hv.xin-giam', target: r.id, hotel: hid, scope: h.province, after: r.data });
lib/he-thong.mjs:322:    await audit(env, { actor, role: staff.role, action: 'hv.xin-bo-sung', target: r.id, hotel: hid, scope: h.province, after: r.data });
lib/he-thong.mjs:417:      const c = await loadCfg(env, h.id); const pend = new Set(s.reqs.filter((r) => r.type === 'oprice' && r.hotel === h.id && r.status === 'cho').map((r) => r.data.item));
lib/he-thong.mjs:487:      if (r.type === 'profile') { before = h[r.data.field]; h[r.data.field] = r.data.to; after = r.data.to; }
lib/he-thong.mjs:488:      if (r.type === 'price') { s.price[h.id] = s.price[h.id] || {}; const p = s.price[h.id][r.data.cls] = s.price[h.id][r.data.cls] || {}; before = p[r.data.kind] ?? null; p[r.data.kind] = r.data.to; after = r.data.to; }
lib/he-thong.mjs:489:      if (r.type === 'oprice') { const c = await loadCfg(env, h.id); before = (c.prices || {})[r.data.item] ?? r.data.from; if (!setPrice(c, r.data.item, r.data.to)) return E(409, 'item_gone', 'Món / tour này không còn trong màn order.'); await saveCfg(env, h.id, c); after = r.data.to; }
lib/he-thong.mjs:490:      if (r.type === 'fee') { before = h.fees || null; h.fees = { ...r.data.to, approvedBy: u.id, approvedAt: vnTime(nowMs(env)) }; after = h.fees; }
lib/he-thong.mjs:492:        const d0 = r.data.date && r.data.date >= today ? r.data.date : today; const d = invDay(s, h, d0, env); const row = d.rows.find((x) => x.cls === r.data.cls);
lib/he-thong.mjs:494:        if (d.cap.used + r.data.qty > d.cap.rooms) return E(409, 'over_cap', 'Duyệt sẽ vượt mức tối đa ' + d.cap.pct + '% quỹ phòng (' + d.cap.rooms + ' phòng/ngày). Còn ' + d.cap.free + ' phòng.');
lib/he-thong.mjs:495:        before = row.hv.qty; setInv(s, h.id, d0, r.data.cls, { hv: row.hv.qty + r.data.qty }); after = row.hv.qty + r.data.qty; r.data.date = d0;
lib/he-thong.mjs:498:        const d0 = r.data.date && r.data.date >= today ? r.data.date : today; const d = invDay(s, h, d0, env); const row = d.rows.find((x) => x.cls === r.data.cls);
lib/he-thong.mjs:500:        const sold = row.hv.sold + row.hv.held; let toQty = Math.floor(+r.data.toQty);
lib/he-thong.mjs:501:        if (r.data.mode === 'close') toQty = sold;
lib/he-thong.mjs:503:        before = { hv: row.hv.qty, locked: row.hv.locked }; setInv(s, h.id, d0, r.data.cls, { hv: toQty, lock: toQty <= sold, lockBy: toQty <= sold ? u.id : null }); after = { hv: toQty, locked: toQty <= sold, label: 'Khách sạn chủ động giảm số lượng' }; r.data.date = d0; r.data.appliedToQty = toQty;
lib/hotel-ops.mjs:87:const hm2m = (s) => { const m = /^(\d{1,2}):(\d{2})$/.exec(String(s || '')); return m && +m[1] < 24 && +m[2] < 60 ? +m[1] * 60 + +m[2] : null; };
lib/hotel-ops.mjs:130:      const row = await db.prepare('SELECT v, at FROM hotel_state WHERE k = ?').bind(key).first();
lib/hotel-ops.mjs:174:    const row=await db.prepare('SELECT v, at FROM hotel_state WHERE k = ?').bind(key).first();
lib/hotel-ops.mjs:250:      const ins = await db.prepare('INSERT OR IGNORE INTO hotel_state (k, v, at) VALUES (?, ?, ?)').bind(st.key, payload, at).run();
lib/hotel-ops.mjs:251:      const ch = ins && ins.meta ? ins.meta.changes : (ins && ins.changes);
lib/hotel-ops.mjs:255:    const uch = upd && upd.meta ? upd.meta.changes : (upd && upd.changes);
lib/hotel-ops.mjs:280: * Existing mechanism was INSERT OR REPLACE (last-write-wins, no version check).
lib/hotel-ops.mjs:289:    for (const ord of o || []) { const m = /^OD-(\d+)$/.exec(ord.id); if (m) maxSeq = Math.max(maxSeq, +m[1]); }
lib/hotel-ops.mjs:308:          const ch = ins && ins.meta ? ins.meta.changes : (ins && ins.changes);
lib/hotel-ops.mjs:313:      const uch = upd && upd.meta ? upd.meta.changes : (upd && upd.changes);
lib/hotel-ops.mjs:316:      const row = await db.prepare('SELECT v, at FROM hotel_state WHERE k = ?').bind(st.key).first();
lib/hotel-ops.mjs:1018:    const adults = +((/(\d+)\s*NL/.exec(bk.guests) || [0, 1])[1]); const max = Math.max(1, adults) * bk.qty;
lib/hotel-ops.mjs:1276:      o.log.push({ at: now, by: ss.s.sid + '/' + who, msg: built.meta.label + ' · bước ' + (o.step || '?') + ' · queued in-app' });
lib/hotel-ops.mjs:1287:        label: built.meta.label,
