Harthliving OTA · bản chữ cho AI · dựng từ vault (nguồn sửa lần cuối 29/09/2026 09:28) · mục lục: https://harthliving-ota.pages.dev/ai/index.txt URL (HTML): https://harthliving-ota.pages.dev/library/tech/metrics-agoda-readme/ [Tổng quan](https://harthliving-ota.pages.dev/) › [Dữ liệu & báo cáo](https://harthliving-ota.pages.dev/library/) › [Tài liệu kỹ thuật](https://harthliving-ota.pages.dev/library/tech/) # 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 Nguồn: `metrics-agoda/README.md` ### `metrics-agoda/` — tách khỏi Booking, gộp sau 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.