Harthliving OTA · bản chữ cho AI · dựng từ vault (nguồn sửa lần cuối 03/10/2026 22:21) · mục lục: https://harthliving-ota.pages.dev/ai/index.txt # Harthliving OTA — toàn bộ nội dung, Phần 39/41 · tiếp: https://harthliving-ota.pages.dev/ai/full/part-40.txt Trong phần này: Agoda Partner Portal (YCS) — cẩm nang điều khiển · Agoda Partner Portal (YCS) — cẩm nang điều khiển · Chrome nào được dùng để làm việc · Chrome nào được dùng để làm việc · Lớp dữ liệu listing — nguồn sự thật duy nhất · Lớp dữ liệu listing — nguồn sự thật duy nhất · Lớp hiệu suất AGODA — kho riêng, cùng cơ chế với Booking · Lớp hiệu suất AGODA — kho riêng, cùng cơ chế với Booking --- # Agoda Partner Portal (YCS) — cẩm nang điều khiển URL: https://harthliving-ota.pages.dev/library/tech/agoda-ycs-automation/ # Agoda Partner Portal (YCS) — cẩm nang điều khiển Đúc từ phiên **05/09/2026**: tạo 18 mẫu tin + bật Message scheduler cho 6 listing. Đây là tài liệu **thao tác** (điều hướng, DOM, bẫy tự động hoá). Luật nội dung tin nhắn ở [`vault/10-rules/ota-messaging-rules.md`](https://harthliving-ota.pages.dev/guide/general/ota-messaging-rules/) §4. Luật set up listing ở [`agoda-listing-playbook.md`](https://harthliving-ota.pages.dev/guide/agoda/agoda-listing-playbook/). Kho tin nguồn ở [`vault/19-Harthliving-OTA/platform-status/messages/agoda/`]. ⚠️ **Mặc định Agoda là CHỈ ĐỌC** — [`vault/10-rules/ota-readonly-rule.md`](https://harthliving-ota.pages.dev/library/tech/ota-readonly-rule/). Ba script ghi bên dưới là **ngoại lệ có chủ đích**, chỉ chạy khi user ra lệnh trực tiếp cho đúng việc đó. ## 1. Điều hướng — URL là đường nhanh nhất | Cần tới | Cách | |---|---| | Hộp thư khách của một property | `https://portal.agoda.com/mldc/en-us/app/hermes/inbox/ycs/` | | Đổi property | **Đổi thẳng `` trên URL** — không cần bấm menu chọn căn ở thanh trên | | Message scheduler | Trong hộp thư → `Automation tools` → `Scheduled messages` | | Auto reply | `Automation tools` → `Auto reply` (**khác** Scheduled messages, chưa khảo sát) | | Template replies | Panel cố định bên **phải** hộp thư, có nút `Create new` | Breadcrumb khi vào scheduler: `Guest messages > Message preference`, hai tab `Auto reply` | `Message scheduler`. **Bảy Agoda ID** (đối chiếu [`../listings.md`](https://harthliving-ota.pages.dev/status/properties/)): `84463574` 181 Tô Hiến Thành · `84463562` 15 Nước Mặn 5 · `84463560` 41 Lê Hy Cát · `63382515` A La Carte 305 · `84510952` A La Carte 404 · `84463577` A La Carte 502 · `96450982` 25 An Thượng 11 (lên sàn 18/09/2026, chế độ YCS khách sạn — hộp thư + scheduler vẫn y như Homes; 3 mẫu + 3 mốc bật 30/09/2026). ## 2. Template replies — mẫu tin 🔴 **Mẫu tin thuộc về TỪNG Hotel ID.** Không dùng chung giữa các property. Cùng một nội dung vẫn phải tạo lại ở mỗi listing → 7 listing × 3 mốc = **21 mẫu**. Tên trùng nhau giữa các căn là cố ý. **Giới hạn:** ô `Name` 100 ký tự · ô `Message` 2.500 ký tự. DOM **không** đặt `maxlength`, nên phải tự đếm. ### Quy ước đặt tên (user chốt 05/09/2026) | Mẫu | Tên | Vì sao | |---|---|---| | Xác nhận đặt phòng | `01 Booking confirmed` | nội dung giống hệt mọi căn → **không gắn tên căn** | | Nhận phòng | `03 Check-in - ` | địa chỉ, mã khoá, Wi-Fi khác nhau → riêng từng căn | | Trả phòng | `05 Check-out - villa` **hoặc** `05 Check-out - A La Carte` | **chỉ hai bản**, chia theo giờ trả phòng và cách khoá cửa | Số `01`/`03`/`05` khớp tên file trong `vault/19-Harthliving-OTA/platform-status/messages/agoda/`, tra ngược được. Tên viết **không dấu**. ### DOM của form mẫu tin | Thứ | Selector | Ghi chú | |---|---|---| | Ô tiêu đề | `input[name="Name"]` **hoặc** `input[placeholder*="title of your template"]` | 🔴 **Có property không có attribute `name`** — phải bắt kèm placeholder | | Ô nội dung | `textarea[name="Message"]` | ổn định hơn | | Nút khi **TẠO** | `Save` / `Discard` | | | Nút khi **SỬA** | `Update` / `Cancel changes` | 🔴 tên khác hẳn — dùng nhầm là không tìm thấy nút | | `Update` bị `disabled` | tới khi React ghi nhận thay đổi | phải **poll vài nhịp**, đừng bấm một lần rồi bỏ | ## 3. Message scheduler — ba mốc gửi Ba hàng, mỗi hàng một công tắc + accordion riêng. **Bấm `Save` của chính hàng đó mới lưu.** | Mốc | Ô `Select a time` | Lựa chọn | |---|---|---| | `Upon booking confirmation` | **không có** | gửi ngay khi đặt phòng được xác nhận | | `At check-in` | *"Select a time before guest check-in"* | `On the day of check-in` · `1 day before` · `2 days before` · `3 days before` | | `At check-out` | *"Select a time after guest check-out"* | `On the day of check-out` · `1 day after` · `2 days after` · `3 days after` | 🔴 **Hai mốc lệch NGƯỢC CHIỀU.** Check-in đếm lùi trước ngày đến, check-out đếm tiến sau ngày đi → **không có cách nào gửi tin trước ngày trả phòng** trên Agoda. Bản Booking gửi tin trả phòng tối hôm trước; bê nguyên mốc đó sang Agoda là chọn nhầm. **Cấu hình đang dùng:** booking = ngay khi xác nhận · check-in = `1 day before check-in` · check-out = `On the day of check-out`. **Mỗi mốc chỉ gắn được MỘT mẫu tin** → phải chọn: `At check-out` dùng cho tin dặn trả phòng **hoặc** tin cảm ơn sau kỳ nghỉ (`1 day after check-out`), không thể cả hai. ## 4. Bẫy tự động hoá qua CDP — đã trả giá Nối Chrome đang đăng nhập qua CDP: xem `scripts/ota-read-booking.mjs` để lấy mẫu kết nối (đọc `DevToolsActivePort`, nối WebSocket, **không gọi `Page.enable`** — treo phiên trên portal OTA). | # | Bẫy | Cách đúng | |---|---|---| | 1 | Nút là `SPAN`/`DIV` chứ không phải `BUTTON` → `.click()` trong `Runtime.evaluate` **vô hiệu** | Dùng `Input.dispatchMouseEvent` theo toạ độ `getBoundingClientRect()` | | 2 | Thiếu `mouseMoved` trước `mousePressed` → **React không nhận click** | Luôn bắn `mouseMoved` → `mousePressed` → `mouseReleased` | | 3 | `input#toggle` là checkbox **ẩn, kích thước 0** → bấm không ăn | Bấm phần tử `[role=switch]` hiển thị; lấy cả 3 theo thứ tự DOM: `0` booking · `1` check-in · `2` check-out | | 4 | Gán `el.value = x` thẳng → React không thấy, `Save` gửi chuỗi rỗng | Native setter: `Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(el,v)` rồi `dispatchEvent(new Event('input',{bubbles:true}))` | | 5 | Đếm nút `Edit` theo chỉ số **toàn trang** → bấm nhầm | Trang có **6 nút tên `Edit`**: 3 của scheduler + 3 của panel Template replies. Tệ hơn: **hàng đang bung ra thì mất nút `Edit` của chính nó**, nên số nút tụt 3→2 và mọi chỉ số lệch. Phải giới hạn tìm kiếm **trong panel scheduler** trước khi đếm | | 6 | Nút dropdown ở chế độ sửa hiện **giá trị đang chọn**, không phải chữ `Select …` | Regex phải nhận cả hai: `/^(Select template\\|0[135] )/i` | | 7 | 🔴 **`--set-method` và `--set-instructions` dùng CHUNG endpoint `ArrivalGuide/instructions`**, mỗi cái ghi `null`/`0` vào trường của cái kia (dính 23/09/2026 ở `15nuocman5`) | Chạy **`--set-method` TRƯỚC**, rồi `--set-instructions` (nó đọc method hiện tại và giữ). Kiểm giữa chừng bằng `--dry`: body phải có `"checkInMethod":5` | | 8 | 🔴 **GET của Agoda bị CACHE.** Ghi mô tả xong đọc lại vẫn ra bản cũ → tưởng ghi hỏng, suýt ghi lại lần hai | Đọc xác minh bằng `fetch(url+'?_='+Date.now(),{cache:'no-store'})`. Đừng kết luận "ghi không ăn" từ một lần GET thường | | 9 | `--set-facilities` nhận **MẢNG PHẲNG** `[{facilityId,isSelected,name,from}]` | Bọc `{"facilities":[…]}` là `plan.map is not a function` | | 10 | Điểm **ảnh phòng ngủ / phòng tắm** không đến từ nhóm Property | Đến từ nhóm **ROOM** (`categoryId 7` + `roomTypeId`). Khuôn 100/100 của `41lehycat`: Property 15 ảnh · Room 16 ảnh với **Bedroom 6–7 + Bathroom 4–5** | | 7 | **Dải cảnh báo chống lừa đảo của Agoda phủ lên nút `Edit` của Template replies** → bấm theo toạ độ rơi vào banner, form không mở (gặp ở 502, 21/09/2026; 305 và 404 cùng lúc đó lại không dính) | Nút này là `BUTTON` **thật** nên `.click()` trong `Runtime.evaluate` ăn. Ưu tiên DOM `.click()`, chỉ rơi về chuột CDP khi DOM không mở được form. Kiểm bằng `document.elementFromPoint(cx,cy)`: trả về `DIV` lạ ⇒ đang bị che | | 8 | **Nút `Edit` của hàng scheduler là `DIV`**, không phải `BUTTON`/`SPAN`/`A` → mọi selector `querySelectorAll('button,span,a')` **bỏ sót hoàn toàn**, báo "không thấy Edit". Ngược với #7, `DIV` này `.click()` **không** ăn | Quét bằng `querySelectorAll('*')` lọc `innerText.trim()==='Edit'`, chọn phần tử **cùng dòng** với nhãn hàng (`\\|y − y_nhãn\\| < 30`) rồi bấm bằng **chuột CDP**. Phân biệt hai bộ bằng toạ độ x: scheduler ở panel trái **x≈1481**, Template replies ở panel phải **x≈1785** | **Chưa thông:** *ghi* vào một hàng scheduler **đã cấu hình**. Công tắc đã bật thì bấm lại **không tắt được**, và đường qua `Edit` vướng bẫy #5. Hàng **chưa** cấu hình thì chạy ngon (15/15 mốc ở 5 listing). → Muốn sửa hàng đã cấu hình: **làm tay**, hoặc viết lại phần tìm `Edit` giới hạn trong panel scheduler. 🟢 **ĐỌC hàng đã cấu hình thì thông rồi (21/09/2026).** Bấm `Edit` của hàng (theo bẫy #8) là khung `Message preview` bên phải hiện **tên mẫu đang gắn + toàn văn thân tin**. Đây là cách rẻ nhất để xác minh một mốc đang gắn đúng mẫu nào — không phải mở form ghi, không chạm `Save`. ⚠️ `scripts/ota-agoda-scheduler.mjs --probe` hiện **báo "không thấy mục Scheduled messages"**: Agoda đã đổi nhãn tab thành **`Message scheduler`**, script vẫn tìm chữ cũ. Chưa sửa. ## 5. Sự thật đã xác minh về hiển thị và nội dung 🟢 **Agoda GIỮ dấu xuống dòng trong mẫu tin.** Đọc ngược `textarea` từ portal thấy đúng ký tự `\n`; panel Template replies hiện rõ từng khối tách dòng. Format emoji + dòng trắng **hiện đúng**. ⚠️ **Khung `Message preview` bên phải nuốt ngắt dòng thị giác** — chữ chạy liền một mạch. Đó là CSS của khung preview, **không phải** tin thật. Đừng kết luận format hỏng từ khung này. 🔴 **Agoda che dữ liệu**: số thẻ · số điện thoại · email · **URL** → thay bằng `X`. Kakao ID / Zalo dạng chữ **có thể lọt lưới** — nhưng lọt là dính luật chống off-platform, xem `ota-messaging-rules.md` §4. Agoda dán sẵn câu mời khách tố cáo về `safety@agoda.com` trong mọi cuộc trò chuyện. 🔴 **KHÔNG có placeholder.** Không chèn được tên khách, không chèn được ngày → mẫu tự động là **chữ chết**. Cách né đang dùng: bỏ hết tên và ngày, thay bằng **mốc tương đối** mà chính scheduler bảo đảm (`tomorrow` cho tin check-in gửi trước 1 ngày, `today` cho tin check-out). 🟢 **Agoda tự dịch tin sang ngôn ngữ khách khi gửi** → không cần làm bản tiếng Hàn. ⚠️ Nhưng máy dịch chạy qua **cả địa chỉ tiếng Việt, tên Wi-Fi và tên thương hiệu** — `Harth Living` có chữ `Living` là từ có nghĩa, dễ bị dịch thành nghĩa đen. **Chưa test.** 🟢 **Scheduler đọc thân mẫu SỐNG, không phải bản chụp lúc gán** (xác minh 21/09/2026 trên 305). Sửa nội dung một mẫu trong `Template replies` là tin tự động của mốc đang gắn mẫu đó **đổi theo ngay** — không phải gỡ ra gắn lại. Bằng chứng: sau khi ghi đè `01 Booking confirmed`, mở hàng `Upon booking confirmation` thì khung `Message preview` hiện đúng câu vừa thêm. → Hệ quả: **đổi câu chữ chỉ cần chạy `ota-write-agoda-templates.mjs --update`**, không đụng scheduler. ⏱️ **Cửa sổ 14 ngày:** không liên hệ được khách sau 14 ngày kể từ ngày trả phòng. ## 6. Ba script | Script | Việc | Chạy | |---|---|---| | [`scripts/ota-write-agoda-templates.mjs`] | tạo / đổi tên / **ghi đè thân tin** mẫu, đọc nội dung từ khối text` trong `vault/19-Harthliving-OTA/platform-status/messages/agoda/*.md` | `--dry` · `--only ` · `--rename` · **`--update`** (sửa mẫu đã có cho khớp vault; so trước khi ghi, mẫu nào đã khớp thì bỏ qua; ghi xong **mở lại form đọc lại** rồi mới báo xong) | | [`scripts/ota-agoda-scheduler.mjs`] | bật 3 mốc + gắn mẫu | `--probe` (không lưu) · `--only ` · `--force` | | [`scripts/ota-agoda-photos.mjs`] | ảnh: đọc · tải gốc · upload · đặt ảnh chính · **chuyển nhóm Property⇄Room** · gán tag (thêm 16/09/2026, xem §8) | `--id ` + `--read` · `--download ` (chỉ đọc) · `--upload plan.json` · `--set-main` · `--reassign … --to room\\|property` · `--tag`, mọi lệnh ghi có `--dry` | **Cả hai cần quyền Bash.** Agent không tự cấp được — đưa dòng này cho user dán vào `/permissions`: `Bash(node scripts/ota-agoda-scheduler.mjs:*) ` 🔴 **Rule không khớp vòng `for` của shell.** `for id in …; do node …; done` bị chặn dù rule đã có. Phải gọi **từng lệnh `node` trần một**, mỗi listing một lượt. **Nội dung tin không gõ trong script** — luôn đọc từ vault, để vault và portal không trôi khỏi nhau. ## 7. Còn chưa biết - `Auto reply` hoạt động ra sao (khác `Scheduled messages`) — chưa mở. - ~~Mã `missingPhotoGroup` chưa giải nghĩa~~ → **đã giải 26/09/2026**, xem bảng nhóm ảnh ở §10. - Agoda bắn tin `At check-out` vào **giờ nào** trong ngày → ảnh hưởng câu mở đầu `Today, before 11:00 AM.` - Máy dịch xử lý địa chỉ tiếng Việt, tên Wi-Fi, tên thương hiệu ra sao. - Bộ lọc có bắt nhầm mã cửa 8 số (``) hay mật khẩu Wi-Fi toàn số thành số điện thoại không. - Hồ sơ YCS đã khai số điện thoại chỗ nghỉ chưa — quyết định câu *"call the property number shown on your booking confirmation"* có đúng không. ## 8. Photos 2.0 — ảnh property và ảnh phòng (thêm 16/09/2026) Trang `portal.agoda.com/en-us/app/setting/photos/`. Toàn bộ API đọc từ bundle `PhotosPage.*.js` + `api.*.js` (theo read-wizard-copy-from-bundle-not-clicks), **không cần bấm nút nào**: | Việc | Gọi gì | Ghi chú | |---|---|---| | Đọc toàn bộ ảnh | `GET /en-us/api/setting//Photos/GetAllPhotos` | mảng nhóm: `categoryId 5` = Property (`roomTypeId 0`) · `7` = Room (`roomTypeId` thật). Mỗi ảnh: `photoId` · `photoToken` · `pictureTypeId` (**6 = ảnh chính**, 5 = property, 7 = room) · `pictureCaptionId` (0 = chưa tag) · `order` | | Bảng tag | `GET …/Photos/GetPhotoCaptions` | 97 tag; hay dùng: 13 Exterior view · 19 Lobby · 23 Reception · 9 Entrance · 21 Swimming pool · 25 Restaurant · 5 Buffet · 22 Bar/lounge · 8 Coffee shop · 11 Fitness center · 27 Spa · 121 Massage · 125 Sauna · 154 Bedroom · 3 Bathroom · 139 Separate living room · 102 Kitchen · 1 Balcony/terrace | | Upload | `POST …/Photos/UploadPhoto` multipart `CategoryId` · `RoomTypeId` · `CaptionId` · `Order` · `FormFile` | trả `{photoId, photoToken, recStatus}` — `recStatus 1` = lên ngay, khác 1 = chờ duyệt. **Gán `CaptionId` ngay lúc upload** thì không dính "Missing tags" | | Tag · đặt ảnh chính · **chuyển nhóm** | `POST …/Photos/UpdatePhotos` `{updatePhotoInfoList:[{photoId, photoToken, roomTypeId, imageType, captionId, order}]}` | cùng một endpoint: đổi `captionId` = tag · `imageType 6, order 1` = ảnh chính (ảnh chính cũ phải hạ về `5`) · đổi `imageType 5↔7` + `roomTypeId` = **chuyển Property⇄Room không cần tải lại** (giữ nguyên gốc) | | Ảnh gốc | lấy `uri` từ API, thay `s=x` bằng `width×height` API trả | **giữ nguyên `ce=`** (ảnh mới `ce=3`; ép `ce=0` → CDN trả 1×1); không có `s=` chỉ được 2048px. **Agoda ép cạnh dài ảnh upload về 2048px** — bộ 6016px của 2024 là đường upload cũ | **Bẫy đã gặp:** - Ảnh chính (`pictureTypeId 6`) **không chuyển nhóm / không xoá được** khi đang là ảnh chính → đặt ảnh chính mới trước (`--set-main`), rồi mới `--reassign`. - Sau mỗi `UpdatePhotos`, server **đánh số lại `order` của cả nhóm Room (1→24)** theo thứ tự riêng của Agoda — `order` trên YCS không phải thứ tự mình đặt, đừng dùng nó làm khoá; dùng `photoId`. - Header chỉ có chuẩn + `Request-Id`/`traceparent`, **không có CSRF** → `fetch()` cùng origin trong tab là đủ; upload đi qua `DOM.setFileInputFiles` vào `` tạm do script tạo (không đụng input của app). - 🔴 **Upload trả 409 "duplicate"** khi file định upload trùng md5 với ảnh **đang có** trên listing (41LHC 17/09: 70 ảnh cũ chính là file vault) → re-encode (thumbnail 2048 + save lại) trước khi upload, rồi mới xoá bản cũ. - 🔴 **Mã ảnh trên CDN = md5 của file upload, và bản ghi khoá theo md5:** tải lại đúng file đã xoá thì Agoda trả **cùng `photoId` cũ** và ảnh **sống lại** trong danh sách (đã trả giá 16/09: agent tải lại `restaurant-01` để dò lỗi, làm sống lại tấm user vừa xoá tay). **Thấy ảnh biến mất → hỏi user trước**, đừng tải lại để thử. - Sau upload, CDN trả ảnh **1×1** và API trả `0x0` trong ít nhất 30 phút — không phải lỗi, là xử lý bất đồng bộ. - **Trang khách (agoda.com) trễ theo:** ngay sau khi đổi ảnh (upload · reassign · tag hàng loạt), card tìm kiếm + gallery trang chi tiết **rút xuống chỉ còn 1–2 ảnh** (`contentImages.hotelImages`), trong khi listing khác vẫn 10. Đo 16/09: 2 ảnh lúc 16:45 → đủ **10 ảnh** (trần của card) lúc 17:10, tức ~30 phút sau lần `UpdatePhotos` cuối. **Đừng kết luận mất ảnh từ trang khách trong vòng 1 giờ sau khi sửa.** Trang chi tiết còn tự chèn 4 ảnh "Nearby attraction" (`providerId 999979`) không phải của mình. - 🔴 **`GET …/Facilities/` bị cache trong tab**: PUT xong đọc lại ngay bằng `fetch()` mặc định vẫn thấy giá trị cũ (16/09 tưởng lưu hỏng). Luôn `fetch(url,{cache:'no-store'})` hoặc thêm `?_=Date.now()` khi đọc lại sau khi ghi. - `photoToken` là handle của từng ảnh — **không lưu vào vault** (luật §9), script `--read` lấy lại được. **Tiêu chí ảnh lên trang khách (đo 16/09/2026 trên 305 + đối chứng 502, 41 LHC):** - Gallery trang chi tiết (`contentImages.hotelImages`) = **toàn bộ Property photos** (305: 11/11) + **chỉ 2 Room photos** (305: 2/24 · 502: 2/24 — cùng mẫu) + 4 ảnh "Nearby attraction" Agoda tự chèn. 22 ảnh phòng còn lại **không** vào gallery — chỉ hiện trong card phòng khi ngày tìm còn phòng (chưa xem được vì hai ngày thử đều Sold out). - **Card tìm kiếm = 10 ảnh đầu của gallery** (trần 10, mọi listing đều 10). - Thứ tự: **ảnh chính property (`pictureTypeId 6`) luôn đứng #1**; từ #2 trở đi **không theo `order` trên YCS** mà theo "AI smart ordering" của Agoda (305: hồ bơi → DELI → spa → nhà hàng → buffet → bar → hồ bơi → gym → massage → hồ bơi bình minh — xen kẽ nhóm Facilities/Dining, đẩy gym/massage xuống cuối). Kéo-thả trên YCS chỉ đổi `order`, chưa thấy tác dụng lên trang khách. - 502 có **4 ảnh trên card không tồn tại trong YCS** → là ảnh Agoda nhập từ Booking.com (đúng như chân trang YCS nói). 305 không có ảnh nhập kiểu này. - Muốn một tấm lên card: **đặt vào Property photos + gắn tag**; muốn nó đứng đầu: **đặt làm Main photo**; vị trí 2–10 không điều khiển được. Lần đầu áp dụng: `alacarte305` 16/09/2026 — hồ sơ [`../properties/alacarte305/agoda.md`](https://harthliving-ota.pages.dev/status/agoda/alacarte305/#doc-agoda), sổ việc `L-101`. Lần hai: `alacarte502` 17/09/2026 — [`../properties/alacarte502/agoda.md`](https://harthliving-ota.pages.dev/status/agoda/alacarte502/#doc-agoda): 30 ảnh (17 Property + 13 Room) tag ngay lúc upload, 3 bản sao nội thất re-encode 2048px cho md5 khác bản Room, xoá 37 ảnh cũ bằng `--delete`. ## 9. Content score (Homes) · Arrival guide · Host profile (thêm 16/09/2026) Script: [`scripts/ota-agoda-content.mjs`] — `--score` · `--read` (chỉ đọc) · `--set-times` · `--set-method` · `--set-bio` (ghi, có `--dry`). ⚠️ Mục này là rubric **Homes**; property ở chế độ khách sạn (xem §10) chấm 11 mục khác. Luật chấm điểm chính thức: [`agoda-homes-content-score-guideline-2025.pdf`](https://harthliving-ota.pages.dev/files/agoda-homes-content-score-guideline-2025.pdf) (Photos 30 · Arrival guide 30 · Amenities 15 · Host 15 · Description 10; điểm refresh ≤24 h). | Việc | API | Ghi chú | |---|---|---| | Điểm từng mục | `GET /mldc/en-us/api/reporting/AnalyticsCenter/ContentScoreDetails/` | 18 mục, `isImproveNeeded` + `improvements[]` (gợi ý chung, không phải tiện ích đã tick) | | Arrival guide đọc | `GET /en-us/api/setting/ArrivalGuide/` | `checkInCheckoutTime` · `checkInDetails` · `directions` · `parking` · `houseRules` | | Giờ nhận/trả | `POST …/ArrivalGuide/checkincheckouttime/` body = khối `checkInCheckoutTime` | giờ dạng `"2:00 PM"`; `null` hiện là "Flexible" trên UI **nhưng không được tính điểm** | | Cách nhận phòng | `POST …/ArrivalGuide/instructions/` body = khối `checkInDetails` | `checkInMethod` 1 Reception · **2 Meet and greet** · 3 Key self-collection · 4 Lockbox · 5 Door code · 6 Secret key · 7 Email · 8 Other | | Host profile | `GET /mldc/en-us/api/iam/HostProfile/ViewModel` · `POST …/HostProfile/SaveHostProfile` | **cấp tài khoản** (chung 6 listing); bio 20–500 ký tự, score đòi ≥100 | | Mô tả property | `POST /en-us/api/setting/PropertyDetails/UpdatePropertyDescription/` `{description}` | client chặn URL (`/(https?:\/\/[^\s]+)/`); lưu là lên ngay, không qua duyệt (đọc lại `GeneralInformation` thấy ngay) | | Facilities | `GET/PUT /mldc/en-us/api/setting/Facilities/` — PUT `{facilities:[{facilityId,isSelected}]}` chỉ gửi dòng thay đổi | 88 mục / 8 nhóm; `isSelected` null = chưa khai · false = đã khai không | | Giường | `POST /en-us/api/setting/RoomSettings/BedConfigurations//` `{bedroomConfigurations:[{productBedTypeId,numberOfBed,bedroomGroupNumber,optionId:1,productBedTypeLocalName:null}],commonRoomConfigurations:[]}` | bedType 1 Single · 3 Double · 4 Queen · 5 King · 8 Sofa; trả `true` | | House rules · parking · directions | `POST …/ArrivalGuide/houserules|parking|directions/` body = đúng khối trong GET | airport transfer: `isOffered=true` **bắt buộc có `feeValue`** (0 = miễn phí); server HTML-encode dấu `'` thành `'` trong parkingInstruction | | **Occupancy phòng** | `GET /en-us/api/setting/ChildRate/getroombypropertyId/` · `POST …/ChildRate/setuproomoccupancy/` `{setAdultChild:true, rooms:[{roomTypeId, adultSettings:[{baseOccupancy, guestAllowed, maxAdult, maxChildren}], extraBedSettings:[{numberOfExtraBed, numberOfBabyCot}]}]}` | 🔴 **Toàn `null` = Agoda bán mặc định 2 người lớn** dù Rooms khai max 4 (305 mất mọi khách 3–4 người tới 16/09). Sau POST, rate plan tự sinh `occupancyRate {1..n}` cùng giá; trang UI `childrenrateocc` lỗi với property Homes, chỉ API chạy; GET đọc lại vẫn null một lúc — tin response POST + `GetRate.occupancyRate` | | Mô tả trên trang khách | `contentDetail.contentInformation.description.short` (GraphQL agoda.com) | **Agoda tự sinh "About us" từ dữ liệu tiện ích**, không đăng mô tả partner (đo 16/09 trước và sau khi đổi mô tả). Mô tả partner chỉ còn ý nghĩa cho Content Score / app; sai tiện ích (vd Free parking) sẽ lộ ngay ở đoạn tự sinh | | Booking settings | `GET /en-us/api/supply-pricing-metadata/BookingSettings/GetViewModel/` · `POST …/UpdateBookingSettings/` body = `{bookTypeId:{isDirty,value,oldValue}, cancellationPolicyId:{…}, ratePlanSetting:{isDirty,value:[{ratePlanId,maxAdvPurchase,minAdvPurchase,minNightsStay,maxNightsStay}],oldValue:[…]}, dateBasedStayConditions:{…}}` | -1 = No limit / không giới hạn; **không bọc `bookingSettingUpdateModel`** (400 "RatePlanSetting field is required") | | Local recommendations | `GET/POST …/ArrivalGuide/thingsnearby/` `{thingsNearby}` | | | **Room spec (diện tích · số WC)** | `POST /en-us/api/setting/RoomSettings/RoomSpecifications//` body = `{maxOccupancy:{isDirty,value,oldValue}, isChildrenAllowed:{…}, maxAllowedFreeChildren:{…}, noOfBathroom:{…}, sizeOfRoom:{…}, externalRoomTypeCode:{…}}` (cùng mẫu dirty của BookingSettings; đọc từ bundle `RoomSetupBasicDetailsPage` + `useSaveRoomSpecifications` 17/09) | `--set-room-spec ":size=50,bathrooms=1"`; trả `true`; GET đọc lại thấy ngay | | Địa chỉ ngôn ngữ khác | trong `PUT PropertyLocation`: `localizedStreetAddresses:[{productAddressId:0, languageId, streetName}]` | ALL_LANGUAGE: 24 = Tiếng Việt; server trả về kèm `productAddressId` thật | - 🔴 **Nhãn trên trang khách lấy từ taxonomy KHÁCH SẠN, cùng id với toggle Homes:** `Keyless access` (272) hiện thành **"Check-in [24-hour]"**; `Gym` (57) hiện **"Fitness center — Free"** (không có cờ paid — `FacilityProperties: []`). Bật toggle Homes là chấp nhận nhãn khách sạn tương ứng. Đối chiếu id ở `kipp …/usefulinformation` → `propertyFacilitiesApi/GetViewModel`. - 🔴 **Ô bio Host profile là React controlled — native setter + dispatch `input` KHÔNG ăn** (ô về 0/500 ngay). Phải `Input.insertText` qua CDP sau khi `focus()` (gõ như người). Bảng `SET_VAL` ở §4 chỉ đúng với form Template replies. | Đổi tên listing | `POST …/PropertyDetails/UpdatePropertyName/` `{propertyName, propertyLocalName}` | lưu ngay, `propertyLanguageName` tự bằng tên mới; luật đặt tên Agoda cấm ngoặc/khuyến mãi — user tự chịu | | Check-in instructions | `POST …/ArrivalGuide/instructions/` body = khối `checkInDetails` (giữ `checkInMethod`) | ≥25 ký tự; **Directions không hiện cho khách**, muốn khách đọc điều kiện thì viết vào instructions/tên | - 🔴 **Airport transfer trên Homes chỉ có 3 ô Có/Phí/Phút, không có ô điều kiện** → bật là Agoda tự viết "Free airport transfer" + FAQ "Yes" + đoạn AI cho MỌI booking. Điều kiện phải gài ở tên listing + check-in instructions + mẫu tin (305, 16/09). **Luật chấm cần nhớ (đọc lại PDF 17/09):** *Check-out Time: Specify the check-out **from and** check-out until time or indicate if it is flexible* → chỉ khai `checkOutUntil` là **0/4** (305 vẫn 0/4 sau khi 91/100). 502 khai cả `checkOutFrom = 6:00 AM` + `checkOutUntil = 12:00 PM` (user chốt 17/09) — chờ refresh để xác nhận. Ảnh bedroom/bathroom **ở nhóm Room vẫn được tính** (305 lên 4/4 + 4/4 chỉ với tag ở Room). **Bẫy 17/09 (502):** `PUT Facilities` trả 200 nhưng GET `no-store` ngay sau vẫn **null cả 88** — PUT lại lần hai → 88/88 khớp. Chưa rõ do xử lý bất đồng bộ hay lần đầu không ăn; **luôn đọc lại và so từng dòng với plan trước khi báo xong**. **Lặp lại ở 404 (23:10): lần 1 chỉ 59/88 ăn** (29 dòng nhóm Guest favorites còn null), PUT lại → 88/88. Coi như quy tắc: **PUT hai lần, đọc lại giữa hai lần**. Cùng ngày: `setuproomoccupancy` **đặt `maxAllowedFreeChildren` về 0** (305 16/09, 404 17/09) → muốn giữ 1 phải chạy `--set-room-spec ":children_free=1"` SAU occupancy. Cùng ngày: 502 đang **Non-refundable (13308)** chứ không phải 949 như 305 — đừng suy chính sách từ căn này sang căn khác, đọc `GetViewModel` trước khi hỏi user. **Mục "Add more photos of your property facilities" (2 điểm, 18/09):** Agoda dò **từng tiện ích đã tick Yes** xem có ảnh gắn đúng caption tag hay chưa và liệt kê tên tiện ích thiếu trong `improvements[]` (41LHC: *Bathtub · Dryer*; 181: *Dryer*). Tag `Bathroom` (3) KHÔNG đếm cho Bathtub — phải là `Bathtub` (136); ảnh máy giặt tag `Washing machine` (144) không đếm cho Dryer — cần thêm ảnh tag `Clothes dryer` (137) dù máy sấy xếp ngay trên máy giặt trong cùng khung. Cách vá rẻ nhất: đổi tag bản Property của ảnh WC có bồn (nhóm Room vẫn giữ đủ Bathroom = số WC) và upload thêm một bản ảnh giặt là re-encode (md5 khác, tránh 409) tag 137. Bảng tag đầy đủ: `GET …/Photos/GetPhotoCaptions` (Private pool 138 · Sauna 125 · Swimming pool 21 · Garden 38 · Fitness center 11 · Hot tub 17 · Parking lot 166). **Bẫy đã trả giá 16/09:** - 🔴 **SaveHostProfile đòi số điện thoại đã xác minh OTP** — handler Submit của form kiểm `phoneVerified && phoneNumber` trước khi gửi; gọi API thẳng khi chưa có SĐT → **500 "Failed to save host profile"**. Agent không xác minh OTP được → **user làm tay** (Host profile → Edit → Phone → Verify). - `photoUrlString` phải rỗng/`null` hoặc `/avatar/<64hex>_<32hex>.jpg` — gửi lại URL `pix6.agoda.net` cũ là **400 regex**; form gốc gửi `null` khi không đổi ảnh. - `userProfileType` chỉ nhận `1` (Individual) / `2` (Company) / `null`. ## 10. Property ở chế độ YCS KHÁCH SẠN (không phải Homes) — thêm 19/09/2026 **Nhận biết:** `GET /mldc/en-us/api/layout//PrivateLayout/Main` → `MenuViewModel.IsNonHotelAccommodation = false` (Homes = true). Dấu hiệu trên UI: star rating có giá trị (5) · menu *Rate plans · Room mapping · Cancellation policies · Children Rate and Occupancy* · trang Facilities hiện **440 mục / 16 nhóm** (Guest favorites 15 · Things to do 47 · Dining 60 · … · Languages 44 · Payment 63) thay vì 88 toggle / 8 nhóm · không có Host profile card. `isNhaEnabled` trong GeneralInformation và `isNhaProperty` trong ArrivalGuide **vẫn true** — không tin hai cờ đó. Lần đầu gặp: `25anthuong11` `96450982`, sinh ra từ *List my property → Fast-track import* (URL Booking) 18/09/2026 — wizard không có bước chọn Hotel/Homes; 5 căn tạo trước đó đều là Homes. Không có toggle/API đổi loại hình → xin support (`L-120`). **Rubric content score bản khách sạn = 11 mục** (CMS id đọc từ bundle `ContentScorePage.*.js`, chữ lấy bằng `POST /mldc/en-us/api/reporting/Cms/MultiGet` body `{languageCode:'en-us',origin:'VN',ids:[…]}`): | CMS | Mục | Trang sửa | |---|---|---| | 522393 · 522394 | Add more photos of your property · high-resolution | Photos (API Photos 2.0 như §8 — dùng chung cả hai chế độ) | | 522395 | Add more photos of your property **facilities** | Photos — tag khớp facility đã Yes (Parking lot 166 ↔ Car park · Kitchen 102 · Washing machine 144) | | 522396 · 522397 · 522398 | photos of your rooms · high-res · photos of your **room amenities** | Photos nhóm Room (Bedroom 154 · Bathroom 3 · Balcony 1 · Kitchen 102) | | 522399 · 522400 | essential / additional **property facilities and services** | `/en-us/kipp/app/settings/propertysetting/propertyservices/` — API vẫn là `GET/PUT /mldc/…/Facilities/` (440 dòng, PUT `{facilities:[{facilityId,isSelected}]}`) | | 522401 | Add **room amenities** | Rooms (kipp) → `POST /en-us//kipp/api/roomtype/UpdateRoomType` | | 522402 | Update your **star rating** | chỉ qua chat Agoda (`#chat`) — import đã gán 5 | | 522403 | guest **age policy** | `/en-us/app/setting/childrenrateocc/` — 4 POST `ChildRate/setup*` | **Nhóm ảnh `missingPhotoGroup` — giải 26/09/2026 từ bảng `ks` trong bundle `es.*.js` của trang Photos** (không bấm thử; kiểm chứng bằng chính API: gắn tag xong, số nhóm tụt ngay trong `GetAllPhotos`): | Nhóm | Caption id thuộc nhóm | Nghĩa | Mục content score dò nó | |---|---|---|---| | 1 | 8 Coffee shop · 25 Restaurant · 165 Restaurant (private room) | Nhà hàng | 522395 (nếu tick tiện ích nhóm Dining) | | 2 | 5 Buffet · 104 Food and beverages | Ăn uống | 522395 | | 3 | 21 Swimming pool · 122 Pool [outdoor] · 138 Private pool | Hồ bơi | 522395 | | 4 | 27 Spa · 32 Beauty salon · 119 Hot spring bath · 121 Massage · 125 Sauna | Spa | 522395 | | 5 | 11 Fitness center · 153 Gym/fitness | Gym | 522395 | | 6 | 36 Kid's club · 37 Playground · 142 Pool [kids] · 151 Kids areas | Trẻ em | 522395 | | **7** | **24 Recreational facilities** · 40 · 41 · 101 · 103 · 105 · 112 · 113 · 114 · 118 · 124 · 126 · 131–135 · 147 · 149 | **"Things to do"** | 522395 — `improvementKeyId 7` = đúng nhóm này | | 8 | 3 Bathroom | Phòng tắm (Room) | 522398 room amenities | | **9** | **110 Bed** | Giường (Room) | 522398 | | **10** | **29 View** | Tầm nhìn (Room) | 522398 | ⇒ Mục **522395 bản khách sạn** đòi ảnh theo **nhóm tiện ích đã tick** (tick Karaoke/Massage chair thuộc nhóm facility *Things to do* → cần ≥1 ảnh Property mang caption nhóm 7, vd `24 Recreational facilities`). Mục **522398** đòi Room có đủ nhóm 8 · 9 · 10 — tag `Bedroom` (154) **không** thay được `Bed` (110). `missingPhotoGroup` liệt kê cả nhóm của tiện ích **không tick** (25AT11 thiếu `[1..7]` nhưng `improvements[]` chỉ nêu *Things to do*) — suy ra chỉ nhóm khớp tiện ích đã tick mới bị chấm; xác nhận khi điểm refresh. Lần đầu áp dụng: 25AT11 26/09 (97 → chờ refresh). Bộ Homes (522415–522432: 18 mục — ảnh 6 · amenities 2 · mô tả · host 3 · arrival guide 6) **không áp** cho property hotel; nhưng Arrival guide-book/mô tả vẫn ghi được bằng API §9 và nên làm sẵn để đổi loại hình không phải làm lại. **API kipp (đọc từ `cdn5.agoda.net/ycs/dist/22-*.js` + `184-*.js`):** - Room: `GET /en-us//kipp/api/roomtype/GetRoomType?roomTypeId=` → `Data.Room` (Facilities = `{Access,Accessibility,Bath,Clothing,Comfort,Dining,Entertainment,Layout,Safety,Others:[id]}` · `IsChildrenAllowed` · `NoOfBathroom` · `SizeOfRoom` · `ViewId` · `RoomBedConfigs`). Bảng id→tên: `…/roomtype/getRoomTypeMasterData` (`Facilities` 10 nhóm · `HotelViews` · `BedTypes`). **Ghi:** `POST …/roomtype/UpdateRoomType` body = đúng object Room + `hotelId, language:'en-us', RoomTypeId` (builder `c=function(e,t,n,l,…)` trong bundle 22). Script: [`scripts/ota-agoda-room-amenities.mjs`] `--read` / `--apply plan.json [--dry]`, tự đọc lại so từng nhóm. Hằng số: `nonSmoking 15 · bathtub 37 · smokingAllowed 281`. Server bỏ qua `MaxAdultsOccupancy/MaxChildrenOccupancy` (null), `NoOfBedroom` tự tính từ bed config. - Property facilities kipp: `GET …/kipp/api/propertyFacilitiesApi/GetViewModel?facilityGroupIds=` · `POST …/UpsertHotelFacilities` — không cần, PUT `/mldc/…/Facilities` ăn cả 440 (19/09: lần 1 khớp 440/440). - Age policy (mldc `ChildRate`, `Content-Type: application/json-patch+json`, body thường): `setupagepolicy` `{minimumAgeAllowed:0, childRateEnabled:true, childAgeRange:[{ageFrom:0, ageTo:2, recStatus:1, needExtraBed:false}]}` → trả `childAgeRanges[].hotelChildAgeRangeId` · `setupuniversalrate` `{mode:1, ageRanges:[{ageRangeId, stayRate:{value:0, type:4}, breakFast:{}}]}` (pricingType 1 Flat · 2 Discount adult · 3 % adult · **4 Free** · 5 Not provided) · `setupextrabed` `{isExtraBedEnabled:false, ageRequireExtraBed:null, ageRanges:[], isChannelManager:false}` · `setuproomoccupancy` như §9 · `setupfacilities` (child facilities, tuỳ chọn). Đọc lại: `childviewmodel` (`ageRange`, `hotelOccupancy.childRateModeId`) + trang wizard hiện **Active**. Bed config vẫn ghi qua mldc `BedConfigurations` (chạy được ở hotel-mode). **Bẫy 19/09:** - 🔴 **Ảnh dọc 2:3 (2000×3000 hay 1365×2048) bị Agoda ép về cao 1536 → rộng 1023**, dưới ngưỡng 1024×768 của mục high-resolution. Resize dọc về đúng **1030×1536** (cắt 9 px) trước khi upload thì Agoda giữ nguyên (181 qua với 1030×1536). - **Ghi hai lần** lặp lại: `UpdateRoomType` lần 1 chỉ ăn `IsChildrenAllowed`, amenities ăn lần 2; `UpdatePropertyDescription` lần 1 đọc lại 0 ký tự, lần 2 ăn. Luôn đọc lại `no-store` so plan. - `ContentScoreDetails` trả **204** cho listing mới (cả 404 hôm 17/09) — không phải lỗi, chờ ≤24 h; trang Analytics hiện "No content score data at the moment". - Tab CDP: `chrome-grab` mở/đổi tab nên script tự viết phải chọn tab theo URL (`TAB=childrenrateocc`), không lấy tab `portal.agoda.com` đầu tiên. **Liên quan:** Agoda — cơ chế đã hiểu · Cẩm nang set up listing Agoda · issues.md L-15 · Luật tin nhắn OTA · Luật chỉ đọc OTA Nguồn: guide-book/platforms/agoda-ycs-automation.md ## Tài liệu kỹ thuật khác - [Luật truy cập OTA và PriceLabs — đọc tự do, ghi phải xin phép](https://harthliving-ota.pages.dev/library/tech/ota-readonly-rule/) - [Chrome nào được dùng để làm việc](https://harthliving-ota.pages.dev/library/tech/chrome-profile-rule/) - [19-Harthliving-OTA/platform-status/messages — Tin nhắn gửi khách qua OTA](https://harthliving-ota.pages.dev/library/tech/ota-messages-readme/) - [Dump trung tâm hướng dẫn PriceLabs — 27/09/2026](https://harthliving-ota.pages.dev/library/tech/2026-09-27-pricelabs-helpcenter-index/) - [Lớp dữ liệu listing — nguồn sự thật duy nhất](https://harthliving-ota.pages.dev/library/tech/data-readme/) - [Lớp dữ liệu HIỆU SUẤT theo thời gian — schema + luật ghi](https://harthliving-ota.pages.dev/library/tech/metrics-readme/) - [Lớp hiệu suất AGODA — kho riêng, cùng cơ chế với Booking](https://harthliving-ota.pages.dev/library/tech/metrics-agoda-readme/) --- # Chrome nào được dùng để làm việc URL: https://harthliving-ota.pages.dev/library/tech/chrome-profile-rule/ # Chrome nào được dùng để làm việc **User chốt 24/09/2026.** Mọi thao tác đọc OTA (Booking · Airbnb · Agoda · PriceLabs) phải chạy trên profile Chrome đang đăng nhập **Huy · huyngstr@gmail.com** (avatar Harth Living). Máy này có nhiều profile Chrome khác — Brandee · Duy · Hanh · Hiếu · Living · Trọng — **không dùng bất kỳ profile nào trong số đó.** ## Vì sao có luật này Các profile khác đăng nhập tài khoản khác, nên vừa không thấy đúng listing, vừa có nguy cơ thao tác nhầm trên tài sản của người khác. ## 🔴 Bẫy lớn nhất: tab mới mở SAI PROFILE, không phải hết phiên Một `user-data-dir` chứa nhiều profile; mỗi profile là một `browserContextId`. **`Target.createTarget` luôn mở ở context MẶC ĐỊNH** — và context mặc định ở máy này **không phải** profile Huy. Nên tab mới chưa bao giờ đăng nhập Booking, mọi trang bị đá về `sign-in`, **trông y hệt phiên hết hạn**. Truyền `browserContextId` của profile cũng không được: `createTarget` chỉ nhận context do chính nó tạo — trả `Failed to find browser context with id …`. **Cách duy nhất chạy được:** nhờ một tab đang đăng nhập mở tab con bằng `window.open(url,'_blank')` với `userGesture: true`. Tab con thừa hưởng đúng profile. `# so hai giá trị này; khác nhau là KHÔNG được tạo tab trần curl -s -X POST http://127.0.0.1:9333/cdp -H 'content-type: application/json' \ -d '{"method":"Target.getBrowserContexts"}' # defaultBrowserContextId curl -s -X POST http://127.0.0.1:9333/cdp -H 'content-type: application/json' \ -d '{"method":"Target.getTargets"}' # browserContextId của tab extranet ` Đã sửa trong `chrome-grab.mjs`, `ota-read-booking-funnel.mjs`, `ota-read-booking-reviews.mjs` (24/09/2026). ## Bẫy đi kèm: nhiều tab = nhiều token `ses` Extranet cấp một token `ses=` cho mỗi phiên. Mở nhiều tab qua nhiều lần đăng nhập thì các tab mang **token khác nhau**, và token cũ **đã chết** nhưng URL vẫn còn đó. `chrome-grab.mjs` lấy token của **tab đầu tiên** tìm thấy — nếu đó là tab cũ thì mọi lệnh sẽ bị đá về trang đăng nhập, trông y hệt "hết phiên". Đã dính 24/09/2026. **Cách tránh:** lấy token của tab **mới nhất**, hoặc đóng hết tab extranet cũ trước khi chạy. `scripts/ota-read-booking-funnel.mjs` đã làm đúng (hàm `session()` lấy token cuối danh sách). ## 🔴 Vào Booking extranet: chỉ đi từ tab đã mở sẵn (user chốt 02/10/2026) User dừng lệnh giữa chừng khi agent tự mở URL extranet mới bằng `chrome-grab.mjs ` (lần đó lấy nhầm token `ses` cũ → trang `reservation_policies` bị đá về `sign-in`). Chỉ được hai cách: 1. **Đọc thẳng tab user đang mở** — `chrome-grab.mjs --tab ` (ví dụ `--tab property_policies`), không điều hướng. 2. **Cần trang khác của một căn** → vào tab **Group homepage** đang mở sẵn, bấm vào link listing của căn đó (hoặc bấm link/menu ngay trong tab extranet của căn đang mở). Trang con thừa hưởng đúng phiên. 🔴 **Cấm:** tự ghép URL `admin.booking.com/...` rồi `Page.navigate`/`Target.createTarget`/tab mới, kể cả khi đã gắn `ses=` — trang mới có thể bắt đăng nhập lại từ đầu. Không có tab phù hợp thì dừng, nhờ user mở trang đó rồi đọc bằng `--tab`. Bấm link/menu chỉ để **điều hướng xem**; mọi nút Save/Edit/Apply vẫn theo `ota-readonly-rule.md`. ## Kiểm nhanh trước khi chạy lô lớn `curl -s -X POST http://127.0.0.1:9333/cdp -H 'content-type: application/json' \ -d '{"method":"Target.getTargets"}' | grep -o 'ses=[a-f0-9]*' | sort -u ` Ra nhiều hơn một token là phải dọn tab trước. Nguồn: 10-rules/chrome-profile-rule.md ## Tài liệu kỹ thuật khác - [Agoda Partner Portal (YCS) — cẩm nang điều khiển](https://harthliving-ota.pages.dev/library/tech/agoda-ycs-automation/) - [Luật truy cập OTA và PriceLabs — đọc tự do, ghi phải xin phép](https://harthliving-ota.pages.dev/library/tech/ota-readonly-rule/) - [19-Harthliving-OTA/platform-status/messages — Tin nhắn gửi khách qua OTA](https://harthliving-ota.pages.dev/library/tech/ota-messages-readme/) - [Dump trung tâm hướng dẫn PriceLabs — 27/09/2026](https://harthliving-ota.pages.dev/library/tech/2026-09-27-pricelabs-helpcenter-index/) - [Lớp dữ liệu listing — nguồn sự thật duy nhất](https://harthliving-ota.pages.dev/library/tech/data-readme/) - [Lớp dữ liệu HIỆU SUẤT theo thời gian — schema + luật ghi](https://harthliving-ota.pages.dev/library/tech/metrics-readme/) - [Lớp hiệu suất AGODA — kho riêng, cùng cơ chế với Booking](https://harthliving-ota.pages.dev/library/tech/metrics-agoda-readme/) --- # Lớp dữ liệu listing — nguồn sự thật duy nhất URL: https://harthliving-ota.pages.dev/library/tech/data-readme/ # Lớp dữ liệu listing — nguồn sự thật duy nhất Mỗi file `data//.json` chứa **toàn bộ sự thật đã xác minh của một căn trên một kênh**, ở dạng máy đọc được. **Khi JSON và Markdown lệch nhau, JSON đúng.** File `.md` trong `properties/` là **lớp trình bày cho người đọc**, sinh ra từ cùng dữ liệu này. ## Vì sao có lớp này Trước 08/09/2026 mọi fact nằm trong văn xuôi Markdown. Ba hệ quả đo được: - **Không tra được bằng máy.** Câu *"Swimming pool → Outdoor · All year · Paid · 06:00–19:00"* là chữ trong bảng; muốn biết bể bơi có thu phí không thì phải đọc hiểu. - **Cùng một con số viết hai kiểu.** Giá buffet xuất hiện **3 lần dạng `285.000₫`** và **3 lần dạng `285,000 VND`** trong cùng một bộ hồ sơ. Grep một dạng là hụt một nửa. - **Không so được giữa các căn.** Muốn biết 502 thiếu gì so với 305 thì phải mở hai file và đọc tay. ## Dùng `python3 scripts/listing-data.py list # có những file nào python3 scripts/listing-data.py check # kiểm khoá bắt buộc + trường còn trống python3 scripts/listing-data.py show alacarte305 # in gọn một listing python3 scripts/listing-data.py compare booking # bảng đối chiếu mọi căn cạnh nhau python3 scripts/listing-data.py gaps alacarte305 # căn khác THIẾU gì so với căn mẫu python3 scripts/listing-data.py get alacarte305 facilities.swimming_pool.paid ` `gaps` là lệnh đáng dùng nhất khi đã có ≥2 căn: lấy một căn làm chuẩn, liệt kê **tiện ích căn kia chưa bật** và **mọi chỉ số thấp hơn căn chuẩn**. ## Bộ khoá | Khối | Chứa gì | |---|---| | `read` | ngày đọc, đọc bằng gì, trỏ tới `_raw/` chứa dump thô | | `identity` | tên, địa chỉ, toạ độ, trạng thái, URL công khai | | `scores` | page score, review tổng + **từng hạng mục**, điểm công ty, điểm của đối thủ cùng toà | | `photos` | tổng, số hiện public, số bị gắn cờ kém, smart ordering | | `room` | loại phòng, diện tích, số phòng ngủ/tắm, **cấu hình giường từng phòng**, sức chứa | | `fees_taxes` | VAT, phí dọn, giá bữa sáng **cả ba nguồn**, giá đỗ xe **khai vs thật** | | `policies` | giờ nhận/trả, thanh toán, trẻ em, nội quy, **từng chính sách huỷ kèm số liệu 90 ngày** | | `facilities` | từng tiện ích: `on` · `paid` · `hours` · `features` · `price_vnd` | | `amenities_room` | danh sách tiện nghi trong phòng | | `accessibility` | khai báo tiếp cận xe lăn | | `messaging` | mẫu tin, lịch gửi, **cờ cảnh báo** như `contains_door_code`, `asks_for_star_rating` | | `sustainability` | đã bắt đầu chưa, chứng nhận | | `profile` | hồ sơ chủ nhà, **độ dài từng ô chữ** | | `public_view` | thứ khách thật sự thấy: huy hiệu, chip, số nhóm tiện ích, **câu phủ định đang chạy** | | `issues` | mã `L-nn` liên quan, tra chéo sang [`../issues.md`](https://harthliving-ota.pages.dev/issues/) | Quy ước: **`null` = chưa đọc được**, khác với `false` = đã đọc và đúng là không. Mọi số tiền là **số nguyên VND**, không kèm ký tự — định dạng để lớp hiển thị lo. ## Schema v2 — lưu đúng cấu trúc trang web (02/10/2026, đang áp: `25anthuong11` · Booking) User yêu cầu dữ liệu lưu **chuẩn tầng như trang hiển thị** để AI đọc nhanh. Mỗi căn × kênh đã khảo sát theo cây có: `platform-status/data// ├─ .json chỉ số dùng chung (`facts`: identity · scores · fees_taxes) + đường dẫn `platformstatus` └─ platformstatus// ├─ _index.json ĐỌC TRƯỚC: dải nhanh (quick_facts) + danh sách mục lớn {id, title, read, file, subs[]} ├─ home.json · rates-availability.json · promotions.json · property.json · boost-performance.json │ mỗi MỤC LỚN (menu cha) một file = đúng một khối trên trang ├─ messages.json trang nguyên văn từng tin nhắn └─ legacy.json dữ liệu v1 cũ + mục "không có" đã lọc khỏi trang (không hiện, chỉ để tra) ` AI cần một mục thì đọc `_index.json` (4 KB) rồi mở đúng file mục đó, không phải đọc cả bộ. Web ghép các file lại thành cây `page` khi dựng. Cây (ghép từ `_index.json` + các file mục): `page ├─ quick_facts dải nhanh: status · hotel_id · read · tiles[] {key, label, value, sub} ├─ sections[] MỤC LỚN = menu cha Booking (Home · Rates & Availability · Promotions · Property · Boost performance) │ {id, title, booking_menu, lead, source, read, children[]} │ └─ children[] nút theo `type`: │ sub MỤC CON = menu con Booking {id, title, booking_path, read, children[]} │ group NHÓM có tiêu đề {title, notes[], cards[]} · cards = nhóm không tiêu đề │ └─ cards[] THẺ {title, status?, items[]} │ └─ items[] {label?, style: list|text|intro, values[]} │ kv bảng nhãn–giá trị {rows[] {label, value, note?}} │ table bảng {columns[], rows[][]} │ split khung chia đôi {columns[[nút], [nút]]} │ figure ảnh {image (đường dẫn _raw), url (/media/shots/…), alt, caption, data?} │ note · para chữ chú thích / đoạn văn └─ message_pages[] trang nguyên văn từng tin {slug, url, name, trigger, language, text{vi,en}} ` **Quy ước chữ:** mọi chuỗi hiển thị là `{"vi": …, "en": …}` (nút VI | EN chỉ chọn một bản); chuỗi không phải dict thì giống nhau ở hai bản; `**…**` là in đậm, `[chữ](url)` là link — **không nhúng HTML**. Giá trị có thể là `{text, href}` (link) hoặc `{text, strong, sub}` (ô bảng). Số liệu gốc để tính toán nằm trong `data` của nút (vd `calendar.data.unbookable_ranges`, `search_card.data`). Mã khoá cửa / mật khẩu Wi‑Fi **không** nằm trong file (đã che). Web chỉ hiện mục CÓ — mục "không có" chỉ còn trong `legacy`. Sửa trang = sửa đúng file mục lớn trong `platformstatus//` (thêm/bớt/đổi thứ tự nút; đổi thứ tự mục lớn ở `_index.json`) rồi dựng lại; `build-ota-portal.py` không tự thêm nội dung ngoài cây. ## Thêm một căn 1. Cào bằng `node scripts/chrome-grab.mjs --out vault/19-Harthliving-OTA/platform-status/properties//_raw/-/ …` 2. Tạo `data//.json` theo đúng bộ khoá trên — thiếu khoá nào thì `check` báo, **không cần sửa script**. 3. `python3 scripts/listing-data.py check` rồi `gaps alacarte305` để thấy ngay căn mới hụt gì. ## Trạng thái hiện tại | Căn | Kênh | Ngày đọc | |---|---|---| | `alacarte305` | booking | 2026-09-08 | Sáu căn còn lại (`alacarte404` · `alacarte502` · `15nuocman5` · `181tohienthanh` · `41lehycat` · `25anthuong11`) **chưa có file dữ liệu** — mới chỉ có hồ sơ Markdown. Nguồn: platform-status/data/README.md ## Tài liệu kỹ thuật khác - [Agoda Partner Portal (YCS) — cẩm nang điều khiển](https://harthliving-ota.pages.dev/library/tech/agoda-ycs-automation/) - [Luật truy cập OTA và PriceLabs — đọc tự do, ghi phải xin phép](https://harthliving-ota.pages.dev/library/tech/ota-readonly-rule/) - [Chrome nào được dùng để làm việc](https://harthliving-ota.pages.dev/library/tech/chrome-profile-rule/) - [19-Harthliving-OTA/platform-status/messages — Tin nhắn gửi khách qua OTA](https://harthliving-ota.pages.dev/library/tech/ota-messages-readme/) - [Dump trung tâm hướng dẫn PriceLabs — 27/09/2026](https://harthliving-ota.pages.dev/library/tech/2026-09-27-pricelabs-helpcenter-index/) - [Lớp dữ liệu HIỆU SUẤT theo thời gian — schema + luật ghi](https://harthliving-ota.pages.dev/library/tech/metrics-readme/) - [Lớp hiệu suất AGODA — kho riêng, cùng cơ chế với Booking](https://harthliving-ota.pages.dev/library/tech/metrics-agoda-readme/) --- # Lớp hiệu suất AGODA — kho riêng, cùng cơ chế với Booking URL: https://harthliving-ota.pages.dev/library/tech/metrics-agoda-readme/ # Lớp hiệu suất AGODA — kho riêng, cùng cơ chế với Booking Cùng cơ chế với [`../metrics/`](https://harthliving-ota.pages.dev/library/data/): bảng chiều + bảng sự kiện, **chỉ thêm không sửa**, payload gốc giữ trong `_raw/`. 🔴 **Tách kho là có chủ ý.** Hai kênh đo những thứ **không quy về nhau được**: cửa sổ khác nhau, định nghĩa "booking" khác nhau, hoa hồng khác nhau. Trộn sớm là mất khả năng truy một con số về đúng nguồn của nó. Gộp là việc của lớp trình bày, làm sau, và làm tường minh. `metrics-agoda/ ├── dim_property.csv 7 căn × kênh agoda (external_id = Agoda hotel id) ├── fact_agoda_summary.csv 84 · mốc × căn × chỉ số × KỲ ├── fact_agoda_daily.csv 178 · mốc × căn × NGÀY × trục (book|stay) ├── fact_agoda_transactions.csv 28 · 1 GIAO DỊCH tiền ├── fact_agoda_reservations.csv 14 · 1 ĐƠN đặt phòng ├── fact_agoda_reviews.csv 8 · 1 REVIEW ├── agoda_ranking.csv 56 · 1 căn × 1 lượt tìm kiếm công khai (thứ hạng Best match) ├── _raw/-agoda/ payload gốc, mỗi căn một file JSON └── README.md file này ` Cào rồi nạp: `node scripts/ota-agoda-metrics-crawl.mjs # → _raw/-agoda/ python3 scripts/ota-agoda-metrics-extract.py # → 5 bảng fact_agoda_* python3 scripts/build-agoda-page.py # → tab Agoda của báo cáo python3 scripts/build-agoda-data-page.py # → tab Data_Agoda ` ## Khoá tự nhiên từng bảng | Bảng | Một dòng là gì | Khoá | |---|---|---| | `dim_property` | 1 chỗ nghỉ × kênh agoda | `(slug, channel)` | | `fact_agoda_summary` | 1 mốc × căn × chỉ số × kỳ | `(snapshot_date, channel, slug, metric, period_type)` | | `fact_agoda_daily` | 1 mốc × căn × ngày × trục | `(snapshot_date, channel, slug, date, basis)` | | `fact_agoda_transactions` | 1 giao dịch tiền | `(channel, slug, transaction_id)` | | `fact_agoda_reservations` | 1 đơn | `(channel, slug, booking_id)` | | `fact_agoda_reviews` | 1 review | `(channel, slug, review_id)` | | `agoda_ranking` | 1 căn × 1 tìm kiếm × 1 lượt đo | `(snapshot_date, search_id, pass_no, slug)` | **Nối đơn ↔ tiền:** `fact_agoda_reservations.booking_id` = `fact_agoda_transactions.reference_number` (kiểm 2/2 trên 181 THT). Danh sách đơn **không kèm tiền**, nên muốn doanh thu từng đơn thì bắt buộc phải nối. `booking_value = 0` nghĩa là **đơn đã huỷ** — 9/28 giao dịch đang như vậy, giống hệt cách Booking xoá giá đơn huỷ. ## Bản đồ endpoint — dò ngày 24/09/2026 Cây menu đầy đủ không cần bấm: `GET /mldc/en-us/api/layout//PrivateLayout/Main`. | Việc | Trang | API | |---|---|---| | Tổng quan | `/app/reporting/dashboard/` | `reporting/PerformanceDashboard/GetPerformanceSummaries/` | | Hạng so đối thủ | cùng trên | `reporting/MarketInsights/RankingAgainstCompetitors/` | | Phân tích | `/app/reporting/analyticscenter/` | `reporting/BookingInsights/GetTimeFrameMetrics/` · `…/GetTopCompetitors/` | | Đơn | `/app/postbook/booking/` | `postbook/Booking/list/` — **POST**, ngày dạng `/Date(ms)/` | | Review | `/app/setting/review/` | `setting/Review/searchreviews/` | | Tiền từng đơn | `/app/finance/transactions/` | `finance/LegacyTransactions//SearchTransactions` | ## Agoda dễ hơn Booking ở hai chỗ **① Ngày là tham số thật.** `RankingAgainstCompetitors?startDate=YYYY-MM-DD&dayRange=N` nhận **ngày tuỳ ý** — thử `startDate=2025-09-01` và `dayRange=365` đều được phục vụ, không bị ép như `date_from` của Booking. Cũng không có chuyện token nonce hay trần 3 tháng mỗi lần hỏi. **② Phân trang tử tế.** `Booking/list` trả `pagedBookingList{totalCount, pageSize, hasNextPage, totalPages}` — không phải đoán như `hasNextPage` rỗng của Booking. ## Và khó hơn ở hai chỗ **① `GetTimeFrameMetrics` chỉ có 4 kỳ cứng, lùi tối đa 30 ngày.** | `timeFrameType` | 1 = theo ngày ĐẶT · 2 = theo ngày Ở | |---|---| | `period` | 1 = 14 ngày qua · 2 = **30 ngày qua** · 3 = 14 ngày tới · 4 = 30 ngày tới | Bù lại, nó trả `daily[]` — **chuỗi theo NGÀY**, thứ Ranking Dashboard của Booking không hề có. **② `periodType` chưa giải mã hết.** `GetPerformanceSummaries` trả `periodSummary[]` với `periodType` 0 · 1 · 2 · 3, và **API không khai nghĩa**. - ✅ **`periodType 1` = *this month*** — xác minh bằng cách đối chiếu với con số hiện trên dashboard của 181 Tô Hiến Thành (Revenue 8.235.938 ₫). - ⬜ `0` · `2` · `3` **chưa chốt**. Nhìn độ lớn thì `3` là kỳ dài nhất, `2` ở giữa — nhưng đó là suy đoán. **Script ghi nguyên số `periodType`, không tự đặt tên.** Muốn chốt thì đối chiếu từng giá trị với nhãn trên dashboard như đã làm với `periodType 1`. `performanceType`: `0` = đêm phòng · `1` = doanh thu · `3` = lượt đặt (suy từ đơn vị và độ lớn, **chưa đối chiếu nhãn**). `2` và `4..7` chưa dùng. ## Số đầu tiên — mốc 24/09/2026 | Căn | Doanh thu `p3` | Lượt đặt `p3` | |---|---|---| | **181 Tô Hiến Thành** | **111.993.840 ₫** | **10** | | 15 Nước Mặn 5 | 36.520.736 ₫ | 3 | | A La Carte 502 | 6.240.000 ₫ | 1 | | 305 · 404 · 41 LHC · 25 AT11 | 0 | 0 | 🔴 **Đây là doanh thu mà báo cáo Booking hoàn toàn không thấy.** 181 Tô Hiến Thành — căn mà bên Booking chỉ có 45,3 tr doanh thu 12 tháng và bị 70% huỷ — **bán được 112 triệu bên Agoda**. Bất kỳ kết luận nào về "căn này yếu" mà chỉ nhìn Booking đều sai. ⚠️ Chưa biết `p3` là cửa sổ nào, nên **chưa được so thẳng với con số 12 tháng của Booking**. Đó là việc phải làm trước khi gộp hai kênh. ## Điều kiện chạy Giống hệt Booking — xem [`/ota-metrics-crawl`] §0: đúng profile Chrome (`Huy`), tab con phải mở bằng `window.open` từ tab đã đăng nhập, daemon CDP còn sống và chưa đầy bộ đệm. Cần **một tab `portal.agoda.com` đang mở**. ## Ngày bắt đầu active — dò 25/09/2026 **Agoda không có trường "ngày lên sóng".** Đã dò cả 27 mục menu YCS: hợp đồng, compliance, property details, payouts, remittances, calendar, property switcher — không trang nào khai. Nên ngày phải suy từ dấu vết, bằng `node scripts/ota-agoda-active-from.mjs`. | Căn | `active_from` | Căn cứ | Hợp đồng sớm nhất | |---|---|---|---| | `181tohienthanh` | **2026-01-23** | 📧 khách hỏi phòng (sớm hơn đơn đầu 19/02 gần một tháng) | 2026-01-19 | | `alacarte305` | **2025-03-06** | đơn đặt sớm nhất (khách hỏi cùng buổi sáng, 10:08) | 2024-12-25 | | `15nuocman5` | **2026-06-19** | đơn đặt sớm nhất (ở 26/06/2026) | 2026-02-02 | | `alacarte502` | **2026-06-22** | đơn đặt sớm nhất (ở 16/07/2026) | 2026-02-02 | | `25anthuong11` | **2026-09-18** | 📧 **Agoda tự khai** — thư *"Your property has been published!"* 19:41 | 2026-09-18 | | `alacarte404` | — | chưa bán được đêm nào · **không thư Agoda nào từng nhắc tên căn này** | 2026-02-02 | | `41lehycat` | — | chưa bán được đêm nào · thư sớm nhất nhắc tên là 08/07/2026, nhưng chỉ là thư marketing | 2026-02-02 | `active_from` là **cận trên quan sát được**: listing chắc chắn đã active *trước hoặc đúng* ngày đó, không có gì bảo đảm nó active *đúng* ngày đó. Cùng quy ước với kho Booking. **Cột `contract_from`** là lần chấp thuận điều khoản sớm nhất của chính căn đó — trần trên của ngày tạo hồ sơ. Để riêng, **không** thế vào `active_from`: ký giấy xong vẫn có thể chưa bật bán. Khoảng cách giữa hai cột nói lên điều đó: 305 ký 25/12/2024 mà mãi 06/03/2025 mới có đơn; bốn căn cùng ký 02/02/2026 nhưng 15 NM5 tới 19/06 mới bán được, còn 404 và 41 LHC tới nay vẫn chưa. 🔴 **Ba căn để trống là "chưa quan sát được", KHÔNG phải "chưa active".** Ba bằng chứng: 1. `PropertyRecStatus = 1` — **giống hệt** bốn căn đang bán được. Không có căn nào ở trạng thái nháp hay bị tạm ngưng. 2. Lịch đang mở và đang có giá: 404 48 ngày · 41 LHC 90 · 25 AT11 88. 3. `totalPages: 0` trên `SearchTransactions` xác nhận **không có đơn thật**, không phải lỗi phân trang (phân biệt với zero-rows-may-mean-logged-out). Nên chỗ trống không phải lỗi đọc, cũng không phải listing hỏng — nó là **hệ quả của cách suy ngày**. Mọi dấu vết Agoda giữ lại đều là dấu vết *khách để lại* (đơn, review, giao dịch, payout). Căn chưa có khách thì không để lại gì để neo. **Thứ suy được là một KHOẢNG, không phải một ngày:** | Căn | Sớm nhất có thể | Muộn nhất có thể | Rộng | |---|---|---|---| | `alacarte404` | 02/02/2026 (ký hợp đồng) | hôm nay | ~8 tháng | | `41lehycat` | 02/02/2026 (ký hợp đồng) | hôm nay | ~8 tháng | | `25anthuong11` | **18/09/2026** (ký hợp đồng) | hôm nay | **~1 tuần** | 25 An Thượng 11 hẹp tới mức coi như đã có đáp số: property tạo bằng Fast-track import và chỉ chấp thuận đúng bản điều khoản hiện hành, nên 18/09/2026 vừa là ngày ký vừa là ngày tạo hồ sơ. Hai căn còn lại thì khoảng quá rộng để gọi là một ngày. **Cách duy nhất làm hẹp thêm:** đợi đơn đầu tiên, hoặc hỏi Agoda support (họ giữ lịch sử trạng thái property, portal thì không). ### Bên trong hợp đồng có gì — mở đọc 25/09/2026 Nút *View contract* mở **modal trong trang** (không phải PDF, không mở tab mới). Nội dung là **văn bản chung của Agoda, không có tên căn và không có ngày ký riêng**. Thứ duy nhất có ngày là dòng đầu — ngày hiệu lực của *phiên bản điều khoản*, giống hệt nhau ở mọi căn: | id | Phiên bản | Ngày hiệu lực ghi trong văn bản | |---|---|---| | `88` | General T&C **6.0** | 08/12/2023 | | `89` | General T&C **7.0** | 21/02/2025 | | `130` | General T&C **8.0** | 08/06/2026 | 🔴 **Đừng lấy ngày trong văn bản làm ngày của căn.** Ngày per-property nằm ở `lastModify` của API, tức **lúc căn đó chấp thuận**, không phải lúc văn bản ra đời. Ví dụ 41 Lê Hy Cát chấp thuận bản 7.0 ngày 02/02/2026 — 11 tháng sau khi bản đó hiệu lực. Hợp đồng riêng của từng căn (**APPA** — *Accommodation Property Participation Agreement*) là thứ đáng đọc nhất thì **nút View và Download đều `disabled`**: chỉ 305 (`1371548`) và 181 THT (`1705775`) có APPA, và cả hai đều không mở được. Bốn căn còn lại không có APPA nào. **Nhưng danh sách phiên bản đã chấp thuận lại khoanh được vùng ngày tạo hồ sơ.** Một căn chỉ phải chấp thuận những bản còn hiệu lực trong đời nó, nên bản CŨ NHẤT nó có nói lên nó ra đời lúc nào: | Căn | Bản cũ nhất đã chấp thuận | ⇒ hồ sơ tạo trong khoảng | |---|---|---| | `alacarte305` | **6.0** (nhận 25/12/2024) | trước 21/02/2025 — căn duy nhất có mặt từ thời 6.0 | | `181tohienthanh` | 7.0 (nhận 19/01/2026) | 21/02/2025 → 19/01/2026 | | `alacarte404` · `alacarte502` · `15nuocman5` · `41lehycat` | 7.0 (nhận 02/02/2026) | 21/02/2025 → 02/02/2026 | | `25anthuong11` | **8.0** (nhận 18/09/2026) | **sau 08/06/2026** → 18/09/2026 | `25anthuong11` **không có bản 7.0** — nếu hồ sơ đã tồn tại thời 7.0 thì bắt buộc phải có. Đó là bằng chứng độc lập cho thấy căn này sinh ra sau 08/06/2026, khớp với hotel id 96450982 cao vọt so với sáu căn kia. ⚠️ Bản `130` được cả sáu căn cũ nhận **đúng ngày 08/06/2026** (305 lúc 13:12, năm căn kia 13:56) — trùng ngày hiệu lực, nên nhiều khả năng là **đóng dấu hàng loạt tự động**, không phải host bấm. Vì vậy ngày của bản 130 chỉ chứng minh "căn đã tồn tại ngày 08/06/2026", không nói gì thêm. Ngược lại `88` và `89` được nhận muộn hơn ngày hiệu lực cả năm → đó là hành vi thật. ### Nguồn thứ năm: hộp thư Gmail — mạnh hơn cả portal `node scripts/gmail-read.mjs --search ""` đọc Gmail qua tab Chrome đã đăng nhập, **không cần OAuth, không cần 2FA**. Hộp thư giữ thứ portal đã xoá. Hai mốc mà portal không cho, Gmail cho: - 📧 **`25anthuong11` = 18/09/2026 19:41** — thư `no-reply@notifications.agoda-messaging.com` tiêu đề *"Your property has been published! Let's get booking."*, mở ra ghi đích danh *"Dear 4BR Villa in An Thuong 500m to MyKhe Beach"*. Đây là **Agoda tự khai ngày lên sóng**, mạnh hơn mọi suy luận — nên `active_from_basis` ghi `declared`, không phải `observed`. Chỉ căn này có thư đó (tìm `from:agoda "has been published"` trả đúng 1 kết quả); sáu căn kia lên sóng trước khi Agoda có loại thông báo này. - 📧 **`181tohienthanh` = 23/01/2026 02:01** — thư *"Inquiry by YOUNGJAE YOON"*. Khách hỏi được thì listing đã hiển thị, mà mốc này **sớm hơn đơn đầu tiên gần một tháng** (19/02). Đã kiểm không có thư khách nào sớm hơn. Đối chiếu chéo: 305 có thư hỏi phòng lúc **06/03/2025 10:08** và yêu cầu đặt lúc **10:20** cùng sáng — khớp `createdDate` 06/03/2025 của giao dịch, tức hai nguồn độc lập cùng ra một ngày. Thư *"Welcome to Agoda"* **25/12/2024** cũng khớp đúng `contract_from` của 305, xác nhận cách đọc cột hợp đồng là đúng. ⚠️ **Thư marketing nhắc tên căn KHÔNG phải bằng chứng lên sóng** — Agoda gửi cho cả property chưa bán được gì. `41lehycat` được nhắc tên từ 08/07/2026 trong thư mời Mega Sale nhưng vẫn chưa có dấu hiệu nào cho thấy nó từng bán. `alacarte404` thì **không thư nào nhắc tên**. ### 🔴 Lịch KHÔNG phải kho lịch sử — đã thử và đã bác bỏ `Calendar/GetAvailability` và `GetRate` **nhận ngày quá khứ tuỳ ý** và trả về đủ 365 ô, nên rất dễ tưởng là lịch sử tồn kho. Không phải. **Phép thử bác bỏ:** A La Carte 305 có đơn tạo 06/03/2025, khách ở 18/03/2025 — vậy mà quét trọn năm 2025 trả **365 ngày đều `remainingRoomNightsAvailable = 0`, `basePrice = null`, `netRoomNightsBooked = 0`**. Quá khứ bị dọn sạch, không phải "chưa từng mở". **Dấu hiệu nhận ra cái bẫy mà không cần phép thử:** sáu căn khác nhau cùng trả `basePrice` bắt đầu đúng một ngày — 26/08/2026. Sáu listing mở bán ở sáu thời điểm khác nhau không thể cùng có giá từ một ngày; đó là ngày cấu hình giá hiện hành, không phải ngày mở bán. Script vẫn quét lịch nhưng in dưới nhãn *chẩn đoán* và **không cho nó tính `active_from`**. ### `period_type` — đã chốt thêm một giá trị `p3` **khớp tuyệt đối** số đơn trong `fact_agoda_reservations` cho cả bốn căn có đơn (181 THT 10 · 15 NM5 3 · 502 1 · 305 0). Tức `p3` đếm đúng tập mà `Booking/list` trả về. Đối chiếu theo ngày nhận phòng thì ba cửa sổ *12 tháng · từ đầu năm · trọn đời* cho cùng kết quả nên **chưa tách được ba khả năng đó** — nhưng biết chắc `p3` không phải "trọn lịch sử giao dịch", vì 305 có 6 giao dịch từ 2025 mà `p3 = 0`. `p0` (181 THT = 2) và `p2` (181 THT = 5, 15 NM5 = 2) **vẫn chưa khớp cửa sổ nào** đã thử — không khớp 7/30/90 ngày, 6/12 tháng, tháng này, từ đầu năm, theo cả ngày đặt lẫn ngày ở. ## Ba bẫy đã trả giá — đọc trước khi sửa script **① `SearchTransactions` mặc định phân trang MỘT DÒNG mỗi trang.** Thiếu `PageSize` thì API trả 1 dòng với `totalPages: 17` và **không báo lỗi gì**. Dấu hiệu bắt được nó: nới rộng khoảng ngày mà số dòng lại **giảm**. Luôn truyền `PageSize` và đối chiếu với `totalPages`. **② Tên trường của Agoda khác Booking.** `checkinDate` (i thường, không phải `checkInDate`), `roomNights` cho sẵn (không phải tự tính từ hai ngày), không có trường tiền trong đơn. Đoán theo thói quen Booking thì bảng vẫn ra đủ dòng — nhưng **mọi cột đều rỗng trừ `booking_id`**. **③ Review gọi trần trả rỗng.** Phải để trang tự gọi rồi bắt ở tầng mạng qua `chrome-grab`. Trang 181 THT ghi "5 reviews" mà API trả 4 — chênh lệch này chưa giải thích được. ## Chưa làm - `period_type` **0 · 2 · 3** chưa giải mã. Cả ba chỉ số (`bookings` · `revenue` · `room_nights`) đều có đủ 4 kỳ, tức 84 dòng thì **63 dòng chưa biết cửa sổ**. Cách chốt: đối chiếu từng giá trị với nhãn trên dashboard, đúng như đã làm với `1`. - Chuỗi theo ngày mới có **một mốc**. Lùi tối đa 30 ngày nên lịch sử dài chỉ tích luỹ được bằng cách cào hằng tuần — bỏ một tuần là đứt đoạn vĩnh viễn, không cào bù được. - **Không có phễu hiển thị / CTR.** Đã dò cả 27 trang trong `PrivateLayout/Main`: không trang nào trả lượt hiện ra, lượt bấm hay CTR. Nên **không so được khả năng thu hút khách** giữa hai kênh — chỉ so được kết quả bán. - `GetTopCompetitors` trả comp set rỗng cho mọi căn (`isCompetitorsTracked: false`) — chưa rõ phải bật ở đâu. - Chưa dò **Calendar** (occupancy · RevPAR) và **Campaigns / Growth**. ## `agoda_ranking` — thứ hạng trên trang tìm kiếm CÔNG KHAI (thêm 26/09/2026) Tên bảng do user đặt (không theo tiền tố `fact_`). Nguồn: `node scripts/ota-agoda-search-rank.mjs --anon` (phát lại GraphQL `citySearch` của agoda.com) + trạng thái mở bán từ `ari/Calendar/GetAvailability` + sức chứa từ `ChildRate/getroombypropertyId`. **Chỉ thêm, không sửa.** Chi tiết lượt đầu: [`../audits/2026-09-26-agoda-search-rank.md`](https://harthliving-ota.pages.dev/library/reports/2026-09-26-agoda-search-rank/). | Cột | Nghĩa | |---|---| | `search_id` · `pass_no` | mã tìm kiếm trong lượt đo (A–D) · lượt 1/2 — Agoda xếp lại mỗi trang nên phải đo ≥2 lượt | | `city_id` · `checkin` · `checkout` · `nights` · `adults` · `rooms` | bộ lọc khách nhập | | `sort` · `personalization` | `best_match` · `anon` = đã xoá searchHistory (vẫn đăng nhập) | | `total_filtered` · `rows_scanned` · `unique_ids` | tổng Agoda báo · số dòng đã duyệt · số id khác nhau (`rows_scanned > unique_ids` = trùng do xếp lại) | | `rank` · `page_approx` · `pct_from_top` | vị trí đầu tiên gặp · trang (~49 dòng/trang) · % tính từ đầu — **có thể >100 %** vì dòng trùng | | `status` | `ranked` · `not_found` (mở bán, đủ sức chứa mà không thấy) · `not_open` (lịch không mở đủ các đêm) · `over_capacity` (khách > sức chứa) · `grouped_under_84463577` (404 không có dòng riêng, hiện chung nhóm với 502) | | `agoda_bookings_confirmed` | số đơn xác nhận trong `postbook/Booking/list` lúc đo (305: danh sách chỉ trả 1 dù có giao dịch từ 03/2025) | | `note` | ghi chú dòng | ⚠️ Dòng `alacarte502` có `status = ranked` là **thứ hạng của cả nhóm 502 + 404** (*An Hai Bac Houses*), không riêng căn 502. Nguồn: performance/metrics-agoda/README.md ## Tài liệu kỹ thuật khác - [Agoda Partner Portal (YCS) — cẩm nang điều khiển](https://harthliving-ota.pages.dev/library/tech/agoda-ycs-automation/) - [Luật truy cập OTA và PriceLabs — đọc tự do, ghi phải xin phép](https://harthliving-ota.pages.dev/library/tech/ota-readonly-rule/) - [Chrome nào được dùng để làm việc](https://harthliving-ota.pages.dev/library/tech/chrome-profile-rule/) - [19-Harthliving-OTA/platform-status/messages — Tin nhắn gửi khách qua OTA](https://harthliving-ota.pages.dev/library/tech/ota-messages-readme/) - [Dump trung tâm hướng dẫn PriceLabs — 27/09/2026](https://harthliving-ota.pages.dev/library/tech/2026-09-27-pricelabs-helpcenter-index/) - [Lớp dữ liệu listing — nguồn sự thật duy nhất](https://harthliving-ota.pages.dev/library/tech/data-readme/) - [Lớp dữ liệu HIỆU SUẤT theo thời gian — schema + luật ghi](https://harthliving-ota.pages.dev/library/tech/metrics-readme/) --- Phần 39/41 · tiếp: https://harthliving-ota.pages.dev/ai/full/part-40.txt