From 6e97778633bfee9012623b46278b63f360c054db Mon Sep 17 00:00:00 2001 From: andy Date: Tue, 21 Jul 2026 19:11:57 +0700 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3V4=20OWNER=20RATE=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTEXT.md | 5 +- PROJECT_STATE.md | 18 +-- docs/project/README.md | 6 +- .../backend-to-frontend-notes.md | 10 +- .../frontend-to-backend-api-requests.md | 8 +- .../integrations/superagent-api-contract.md | 10 +- .../M002-v4-agent-callback-field-contract.md | 22 ++- ...002-v4-order-task-card-domain-model-cp2.md | 18 +-- .../M002-v4-real-catalog-lookup-api-design.md | 143 ++++++++++++++---- .../M002-v4-test-machine-smoke-checklist.md | 2 +- .../security-access-control-boundary.md | 6 +- .../ReservationV4CatalogBootstrapRunner.java | 136 ++++++++++++++--- .../V25__align_owner_rate_catalog_data.sql | 105 +++++++++++++ ...eservationFrontendQueryControllerTest.java | 10 +- ...servationV4CatalogBootstrapRunnerTest.java | 17 +++ ...ervationV4CatalogLookupControllerTest.java | 26 ++-- .../ReservationV4CommandControllerTest.java | 80 +++++----- .../ReservationV4QueryControllerTest.java | 32 ++-- .../SuperAgentTaskResultControllerTest.java | 54 +++++-- 19 files changed, 520 insertions(+), 188 deletions(-) create mode 100644 server/src/main/resources/db/migration/V25__align_owner_rate_catalog_data.sql diff --git a/CONTEXT.md b/CONTEXT.md index c566aab..b3d7074 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -46,8 +46,9 @@ TH Hotel Simple 是一个前后端分离的酒店业务协同项目。 - Reservation Order Task / Card:M002 V4 后续采用的订单任务与多卡模型;一封来源邮件可按 `order_ref` 形成多个订单任务,每个订单任务下包含来源邮件展示卡、Basic Information 卡和若干业务卡。 - Room Information Card:V4 订单任务中由 New Booking、Update Booking 或 Cancel Booking 触发的房型信息卡;它展示订单房型、日期、早餐、晚数和 Group 状态等可确认业务信息。 - Review Required Card:V4 订单任务中需要人工复核的原业务卡状态;用户仍在原卡片内检查和修正业务字段,完成后确认卡片,不另建独立复核任务卡。 -- Reservation Account:预订业务中的公司、旅行社或客户账户,不是系统登录账号;它用于订单级 Basic Information,并影响可用 Rate Code 范围。 -- Rate Code Applicability:Rate Code 的业务适用范围;当前已确认先按 Reservation Account + booking type(GROUP / FIT)确定候选,不把全酒店 Rate Code 当成所有 Account 通用。 +- Reservation Account:预订业务中的公司、旅行社或客户账户,不是系统登录账号;它用于订单级 Basic Information,并派生 Market / Source。 +- Room Type Catalog:预订业务可选房型代码目录;第一阶段已确认稳定业务集合为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,暂不按 Account 限制可用房型。 +- Rate Code Applicability:Rate Code 的业务适用范围;第一阶段暂不按 Reservation Account 或 booking type 限制候选,Rate Code 先作为当前酒店级目录校验。未来如需求方明确 Account 专属候选,再单独建立适用关系。 - Rooming List Task Card:V4 订单任务中的 Rooming List 事项确认卡,表示当前来源消息包含需要人工处理的房表事项;它不同于独立的 Rooming List Excel 生成工具。 - Payment Attachment Preview:Payment 卡中的付款凭证附件展示能力;业务事实仍是 `attachment_ids[]` 关联,图片可缩略图和大图预览,非图片统一文件列表和下载,附件外链必须走 SourceMessage 原文权限链路。 - Source Message Notification:M002 V4 的 S10 纯通知模型;只表示来源邮件需要被查看和确认已处理,不形成订单任务或业务卡。 diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 0725749..9d9dae4 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -4,16 +4,16 @@ | --- | --- | | 最近更新 | 2026-07-21 | | 当前分支 | `feature/huangting` | -| 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口、V4 业务审计查询、停止旧任务双写、Debug EML V4 profile 对齐、Room Information 后端展示模型与前端业务化展示、V4 任务详情 smoke 修复、Rooming List 确认自动 DEF 后端联动、Rooming List 前端轻量事项卡、Room Information 复核 pointer 与任务详情安全边界修复、Room Information 复核 pointer 运行时规则收口、复核 pointer 部署证明与运行时 trace、Account + booking type 过滤 Rate Code 文档口径、Payment 附件预览后端安全摘要与前端预览接入、Trace 卡后端字段契约收口、Rooming List 事项确认卡文档口径,以及 V4 复核态卡片交互和字段白名单文档口径 | -| 当前重点 | M002 V4 已停止普通业务入站双写旧 `workflow_reservation_task`,V4 后新业务主线只写 V4 order task / cards / source notification;Debug EML V4 smoke 默认复用实时 AgentBus V4 Open API subject,避免误走历史 Debug V2/V3 profile。开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后续上线前单独设计。`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;旧 `open_task_count` / `next_processable_task_id` 仅作历史诊断兼容。Room Information 已完成后端稳定展示模型和前端业务化展示:`GET /api/reservation/order-tasks/{orderTaskId}` 在 `display_payload.room_information` 返回 New / Update / Cancel 的 `current_values`、`proposed_values`、`final_values`、`change_summary[]`,前端只消费该展示模型和 `fields[]`,不再从 Agent raw payload、`business_fields` 或 `target_order` 自行推导;如果卡片 payload 已经是稳定 `room_information.final_values` 结构,后端会按稳定模型归一化查询和复核;Nights、Breakfast 和 Group Booking Status 均以后端派生值为准;确认和复核写入稳定 `confirmed_payload_json.room_information.final_values`,不回写 Agent 原始 `target_order`、Adult、邮件正文或附件 URL;接口对前端暴露的 `fields[].write_target` 使用 `confirmed_payload` / `review_resolution.field_overrides` 这类安全语义,不暴露内部列名;查询侧 `fields[].editable` 和命令侧 `review-resolution` 复核 pointer 校验已共用同一套 Room Information 字段策略。Trace 卡后端契约已收口:普通事项内容字段统一为 `trace_items[].text`,不使用 `content`;`department_code` 第一版只允许 `FO`、`HSK`、`FO+HSK`,任务详情字段会返回 `options_source=reservation_v4_trace_department_fixed` 和 `fixed_options[]` 三个固定选项;`EXTRA_BED.target_room_type_code` 只校验当前酒店 Room Type 目录存在,暂不校验当前订单已有房型;确认和复核共用同一套 Trace 字段白名单,`display_payload` / `confirmed_payload` 不返回 `target_order`、邮件正文、附件 URL、raw evidence 或 AI 原始 payload。复核 pointer 拒绝前会记录 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,包含 order task、card、incoming pointer、query-side editable pointers、command-side allowed pointers、validation error pointers 和 reject reason,但不记录 payload、邮件正文或附件 URL。V4 任务详情 smoke 修复已完成:页面顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面底部,只通过 SourceMessage conversation 接口定位当前触发邮件并默认折叠正文;Basic Information 和普通业务卡的展示 / 确认 payload 不再返回 Agent `target_order`,普通业务卡还会移除邮件 HTML、raw evidence、附件原始 URL 和 PMS 原始响应等敏感字段。Rooming List 卡确认时已实现 Group 自动置 `DEF`:如同订单存在可更新的已确认 Room Information 快照,后端会覆盖其 `group_booking_status=DEF` 并写 `V4_ROOMING_LIST_AUTO_DEF` 审计;刷新任务详情时 `display_payload` 和 `confirmed_payload` 均以 DEF 后的确认快照为准;当前订单详情 `order_overview` 不返回 Group Booking Status 字段;如没有可更新投影,Rooming List 确认仍成功,只写安全审计提示,不临时创建不完整 Room Information。V4 任务详情已支持 ROOMING_LIST 轻量事项卡:页面只显示 “Rooming List / 房表事项”、目标订单线索、人工处理说明和确认按钮,不展示 rows、名单明细、附件预览、导入 / 生成入口、AI payload、邮件正文或附件 URL;PENDING_CONFIRM 确认只提交 `version`,成功后完全使用后端刷新详情,不由前端自行设置 `group_booking_status=DEF`。已确认 Rate Code 下一阶段按 Reservation Account + `booking_type`(GROUP / FIT)过滤和校验,不按全酒店 Rate Code 全量展示;Payment 卡已在 `display_payload.payment_attachments[]` 返回付款凭证附件安全摘要,字段只包含附件 ID、文件名、类型、大小、是否图片、是否理论可预览 / 下载和可选 `external_media_id`,前端在 V4 任务详情 Payment 卡中按当前触发 SourceMessage 的 conversation 附件匹配缩略图、大图预览和非图片下载,匹配优先 `external_media_id` / `externalMediaId`,其次 `attachment_id`,不按文件名猜测;`attachment_ids[]` 第一版仍只读,前端不增删或替换附件集合,Payment 确认只提交 `version`,不提交附件 ID、附件 URL 或完整附件对象;真实 URL 仍只来自 SourceMessage 原文权限链路,权限不足或 conversation 失败时降级展示不可预览 / 不可下载。已确认 `REVIEW_REQUIRED` 仍是原业务卡复核态,页面按钮统一叫“确认卡片”,复核态允许编辑当前卡 `fields[]` 白名单内业务字段,问题字段红字提示。后续可继续做测试机 V4 smoke 复测、Account 范围 Rate Code lookup、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 | +| 当前阶段 | M002 V4 入站、多卡模型、持久化基线、入站写入、查询接口、卡片确认、复核解阻、目录校验、订单详情 V4 总览、DB 目录、Lookup API、前端 lookup 接入、目录管理后台 CP1 前后端、订单列表 V4 继续处理入口 / open count 收口、V4 业务审计查询、停止旧任务双写、Debug EML V4 profile 对齐、Room Information 后端展示模型与前端业务化展示、V4 任务详情 smoke 修复、Rooming List 确认自动 DEF 后端联动、Rooming List 前端轻量事项卡、Room Information 复核 pointer 与任务详情安全边界修复、Room Information 复核 pointer 运行时规则收口、复核 pointer 部署证明与运行时 trace、OWNER RATE Room Type / Rate Code 目录口径、Payment 附件预览后端安全摘要与前端预览接入、Trace 卡后端字段契约收口、Rooming List 事项确认卡文档口径,以及 V4 复核态卡片交互和字段白名单文档口径 | +| 当前重点 | M002 V4 已停止普通业务入站双写旧 `workflow_reservation_task`,V4 后新业务主线只写 V4 order task / cards / source notification;Debug EML V4 smoke 默认复用实时 AgentBus V4 Open API subject,避免误走历史 Debug V2/V3 profile。开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后续上线前单独设计。`GET /api/reservation/orders` 可返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示字段 `open_work_item_count`;旧 `open_task_count` / `next_processable_task_id` 仅作历史诊断兼容。Room Information 已完成后端稳定展示模型和前端业务化展示:`GET /api/reservation/order-tasks/{orderTaskId}` 在 `display_payload.room_information` 返回 New / Update / Cancel 的 `current_values`、`proposed_values`、`final_values`、`change_summary[]`,前端只消费该展示模型和 `fields[]`,不再从 Agent raw payload、`business_fields` 或 `target_order` 自行推导;如果卡片 payload 已经是稳定 `room_information.final_values` 结构,后端会按稳定模型归一化查询和复核;Nights、Breakfast 和 Group Booking Status 均以后端派生值为准;确认和复核写入稳定 `confirmed_payload_json.room_information.final_values`,不回写 Agent 原始 `target_order`、Adult、邮件正文或附件 URL;接口对前端暴露的 `fields[].write_target` 使用 `confirmed_payload` / `review_resolution.field_overrides` 这类安全语义,不暴露内部列名;查询侧 `fields[].editable` 和命令侧 `review-resolution` 复核 pointer 校验已共用同一套 Room Information 字段策略。Trace 卡后端契约已收口:普通事项内容字段统一为 `trace_items[].text`,不使用 `content`;`department_code` 第一版只允许 `FO`、`HSK`、`FO+HSK`,任务详情字段会返回 `options_source=reservation_v4_trace_department_fixed` 和 `fixed_options[]` 三个固定选项;`EXTRA_BED.target_room_type_code` 只校验当前酒店 Room Type 目录存在,暂不校验当前订单已有房型;确认和复核共用同一套 Trace 字段白名单,`display_payload` / `confirmed_payload` 不返回 `target_order`、邮件正文、附件 URL、raw evidence 或 AI 原始 payload。复核 pointer 拒绝前会记录 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,包含 order task、card、incoming pointer、query-side editable pointers、command-side allowed pointers、validation error pointers 和 reject reason,但不记录 payload、邮件正文或附件 URL。V4 任务详情 smoke 修复已完成:页面顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面底部,只通过 SourceMessage conversation 接口定位当前触发邮件并默认折叠正文;Basic Information 和普通业务卡的展示 / 确认 payload 不再返回 Agent `target_order`,普通业务卡还会移除邮件 HTML、raw evidence、附件原始 URL 和 PMS 原始响应等敏感字段。Rooming List 卡确认时已实现 Group 自动置 `DEF`:如同订单存在可更新的已确认 Room Information 快照,后端会覆盖其 `group_booking_status=DEF` 并写 `V4_ROOMING_LIST_AUTO_DEF` 审计;刷新任务详情时 `display_payload` 和 `confirmed_payload` 均以 DEF 后的确认快照为准;当前订单详情 `order_overview` 不返回 Group Booking Status 字段;如没有可更新投影,Rooming List 确认仍成功,只写安全审计提示,不临时创建不完整 Room Information。V4 任务详情已支持 ROOMING_LIST 轻量事项卡:页面只显示 “Rooming List / 房表事项”、目标订单线索、人工处理说明和确认按钮,不展示 rows、名单明细、附件预览、导入 / 生成入口、AI payload、邮件正文或附件 URL;PENDING_CONFIRM 确认只提交 `version`,成功后完全使用后端刷新详情,不由前端自行设置 `group_booking_status=DEF`。OWNER RATE `RATECODE (2)` 已确认第一阶段 Room Type 稳定集合为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,不建立 Account -> Room Type 关系;Rate Code 第一阶段暂不建立 Account 适用关系,Q.B.D 与 LIAN TAI 的 40 个规范化 Rate Code 仅作为当前酒店级 `RATE_CODE` 目录候选维护。Payment 卡已在 `display_payload.payment_attachments[]` 返回付款凭证附件安全摘要,字段只包含附件 ID、文件名、类型、大小、是否图片、是否理论可预览 / 下载和可选 `external_media_id`,前端在 V4 任务详情 Payment 卡中按当前触发 SourceMessage 的 conversation 附件匹配缩略图、大图预览和非图片下载,匹配优先 `external_media_id` / `externalMediaId`,其次 `attachment_id`,不按文件名猜测;`attachment_ids[]` 第一版仍只读,前端不增删或替换附件集合,Payment 确认只提交 `version`,不提交附件 ID、附件 URL 或完整附件对象;真实 URL 仍只来自 SourceMessage 原文权限链路,权限不足或 conversation 失败时降级展示不可预览 / 不可下载。已确认 `REVIEW_REQUIRED` 仍是原业务卡复核态,页面按钮统一叫“确认卡片”,复核态允许编辑当前卡 `fields[]` 白名单内业务字段,问题字段红字提示。后续可继续做测试机 V4 smoke 复测、OWNER RATE 目录导入、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 | ## 1. 当前 Checkpoint -- 名称:`M002-V4-trace-card-backend-contract-tightening` -- 状态:Done,本地已完成 Trace 卡后端字段契约收口。 -- 目标:统一 Trace 卡字段模型,`trace_items[].text` 作为正式内容字段,`department_code` 固定 `FO` / `HSK` / `FO+HSK`,并让查询、确认和复核共用同一套字段白名单。 -- 边界:本 checkpoint 不做 Department 目录 / lookup API、不做 PMS / OPERA / OHIP、不改前端、不推进 M011;EXTRA_BED 目标房型第一版只校验当前酒店 Room Type 目录存在。 -- 联调备注:Trace 专属卡前端可直接消费 `fields[]` 中 `/trace_items/{index}/...` 指针;确认和复核只提交这些 pointer 对应字段,不提交 `content`、`target_order`、邮件正文、附件 URL 或 raw payload。 +- 名称:`M002-V4-owner-rate-catalog-data-alignment` +- 状态:Done,本地已完成 OWNER RATE Room Type / Rate Code 固定种子、V25 数据收口脚本、V4 测试 fixture、文档同步和全量后端测试验证。 +- 目标:将 V4 Room Type 初始化 / 测试种子收敛为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,将 Rate Code 初始化 / 测试种子收敛为 OWNER RATE `RATECODE (2)` 的 40 个规范化酒店级 code。 +- 边界:第一阶段不建立 Account -> Room Type / Rate Code 适用关系,不新增 lookup 过滤参数,不做价格计算、不改 Breakfast 派生规则、不接 PMS / OPERA / OHIP、不推进 M011。 +- 联调备注:前端继续通过 `GET /api/reservation/lookups/room-types` 和 `GET /api/reservation/lookups/rate-codes` 获取选项,不硬编码 OWNER RATE;测试 / 开发库若有人工旧目录 ACTIVE 项,可按目录设计文档第 16 节限定 SQL 停用。 ## 2. 当前优先级 @@ -36,12 +36,12 @@ - 后续每完成一个 Feature 或 Checkpoint,需要更新本文件,避免项目状态继续沉淀在聊天记录里。 - M010 Rooming List Excel 生成后端 CP1 和前端 V1 已实现:前端 `/reservation/rooming-lists/new` 上传来源名单和手工字段,后端同步生成 `.xlsx` 直接下载,第一版不落库、不上传 OSS。 - M011 Booking Excel 附件预处理 CP1/CP2/CP3 已实现:后端可排除人员名单类 Excel,按最近 6 个月候选窗口选择实际存在的最新 3 个业务月,抽取 Booking Update / 附加费表高亮行业务 JSON;Debug EML 和 AgentBus dispatch 在各自 include 开关与总开关同时启用时,会在调用 SuperAgent 前追加 `attachment_extractions[]`。测试机 AgentBus 增强已开启;生产链路仍默认关闭,生产开启需单独确认。 -- M002 V4 CP1 当前已完成入站解析和现有任务链路过渡适配;M002 V4 CP2 已完成订单任务与多卡领域模型设计;M002 V4 CP3 已完成 V4 订单任务、多卡和 S10/S99 来源通知表结构与 Repository 基线;M002 V4 CP4 已完成入站写入新模型;M002 V4 CP5 已完成前端查询接口并补齐订单详情 `v4_order_tasks[]` 时间线;M002 V4 CP6 已完成普通卡片确认和 S10/S99 来源通知 ack;M002 V4 CP7 已完成 `REVIEW_REQUIRED` 卡复核解阻和复核场景订单归属确认;M002 V4 CP8 已完成目录校验、V4 卡片 `fields[]` 字段白名单、确认写入白名单收口和嵌套业务字段目录校验;M002 V4 CP11 已完成数据库目录、初始化种子、启动补种子、Account / Room Type / Rate Code lookup API,并把 V4 入站、确认、复核目录校验切换到当前酒店数据库目录;M002 V4 CP12 已完成前端 lookup 接入第一版和 V4 订单任务时间线消费;M002 V4 CP13 目录管理后台 CP1 已完成前后端列表、新增、启用 / 停用闭环;M002 V4 CP14 已完成订单列表 V4 继续处理入口字段和前端入口消费,`GET /api/reservation/orders` 返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示计数 `open_work_item_count`,前端按 V4 优先跳转,并按 `open_work_item_count` 展示待处理数量;V4 业务审计查询已补齐订单任务审计和来源通知 ack 审计两个只读接口;订单详情已补齐并完成前端接入 V4 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`;V4 普通业务入站已停止双写旧 `workflow_reservation_task`;Room Information 后端展示模型和前端业务化展示第一版已完成;Rooming List 确认触发 Group Booking Status 自动置 `DEF` 已完成,前端轻量事项确认卡也已完成;Payment 附件安全摘要后端和前端预览接入均已完成。Account + booking type 过滤 Rate Code 仍是待补齐缺口;真实 PMS 同步和 SuperAgent 目录机器接口仍未完成。 +- M002 V4 CP1 当前已完成入站解析和现有任务链路过渡适配;M002 V4 CP2 已完成订单任务与多卡领域模型设计;M002 V4 CP3 已完成 V4 订单任务、多卡和 S10/S99 来源通知表结构与 Repository 基线;M002 V4 CP4 已完成入站写入新模型;M002 V4 CP5 已完成前端查询接口并补齐订单详情 `v4_order_tasks[]` 时间线;M002 V4 CP6 已完成普通卡片确认和 S10/S99 来源通知 ack;M002 V4 CP7 已完成 `REVIEW_REQUIRED` 卡复核解阻和复核场景订单归属确认;M002 V4 CP8 已完成目录校验、V4 卡片 `fields[]` 字段白名单、确认写入白名单收口和嵌套业务字段目录校验;M002 V4 CP11 已完成数据库目录、初始化种子、启动补种子、Account / Room Type / Rate Code lookup API,并把 V4 入站、确认、复核目录校验切换到当前酒店数据库目录;M002 V4 CP12 已完成前端 lookup 接入第一版和 V4 订单任务时间线消费;M002 V4 CP13 目录管理后台 CP1 已完成前后端列表、新增、启用 / 停用闭环;M002 V4 CP14 已完成订单列表 V4 继续处理入口字段和前端入口消费,`GET /api/reservation/orders` 返回 V4 下一步订单任务、卡片、动作类型、动作状态、V4 open 数和统一展示计数 `open_work_item_count`,前端按 V4 优先跳转,并按 `open_work_item_count` 展示待处理数量;V4 业务审计查询已补齐订单任务审计和来源通知 ack 审计两个只读接口;订单详情已补齐并完成前端接入 V4 `order_overview`、`next_v4_action`、`related_source_messages[]` 和 `v4_order_tasks[].cards[]`;V4 普通业务入站已停止双写旧 `workflow_reservation_task`;Room Information 后端展示模型和前端业务化展示第一版已完成;Rooming List 确认触发 Group Booking Status 自动置 `DEF` 已完成,前端轻量事项确认卡也已完成;Payment 附件安全摘要后端和前端预览接入均已完成。OWNER RATE Room Type / Rate Code 目录口径已落文档;真实 PMS 同步和 SuperAgent 目录机器接口仍未完成。 - M002 V4 CP2 已确认:V4 工作台统一列表草案为 `/api/reservation/workbench-items`,业务订单任务接口新开 `/api/reservation/order-tasks/**`,S10/S99 来源通知详情草案为 `/api/reservation/source-notifications/{notificationId}`;S10/S99 使用来源通知模型,不再挂隐藏技术订单;`FIT + BOOKING_CODE` 不建 ACTIVE 唯一约束,匹配多条进人工复核;Basic Information 必须先确认;Rooming List 卡第一版只做事项确认;Account / Market / Source 当前通过数据库目录读取和派生;旧 V2/V3 任务详情和草稿确认接口后续可逐步废弃。 ## 5. Next Steps -- 后续如继续做 M002 V4,可优先进行测试机联调,或推进真实 PMS / OPERA / OHIP 目录同步和 `workflow_reservation_catalog_sync_run` checkpoint。 +- 后续如继续做 M002 V4,可优先进行测试机联调,或推进真实 PMS / OPERA / OHIP 目录同步、`workflow_reservation_catalog_sync_run` checkpoint 和 SuperAgent 目录供给方案。 - 后续新增重要功能时,优先在 `docs/project/requirements/` 或未来 `docs/specs/` 中形成 Spec,再实现代码。 - M010 后续如需预览、历史记录、OSS 下载、订单 / 任务预填或客户字段目录化,再单独开前后端 checkpoint。 - M011 CP4 暂不推进;当前停留在 CP3 边界,只增强 SuperAgent 输入,不直接落订单、任务或长期解析历史。后续如确实需要运营查询或长期追踪,再单独设计 Excel 解析批次 / 行级持久化表。 diff --git a/docs/project/README.md b/docs/project/README.md index 4d02422..cebb2cb 100644 --- a/docs/project/README.md +++ b/docs/project/README.md @@ -46,8 +46,8 @@ | `requirements/M002-order-task-workflow-v3.md` | 当前有效 | M002 订单任务主流程 V3,基于 2026-07-11 P0 冻结基线和 2026-07-12 P0.1 Parent Group 修订,记录 S10/S99、40 路由、方案 C、type-known manual review 同卡解阻和 fail-closed 边界。 | | `requirements/M002-task-field-control-contract-v1.md` | 当前有效 | M002 任务卡字段控件契约 V1,记录任务详情 `fields[]` 控件元数据、人工复核控件复用和前后端开发边界。 | | `requirements/M002-v4-agent-callback-field-contract.md` | 当前有效 | M002 V4 Agent 回调字段契约,基于 2026-07-18 业务基线和最新答复,冻结 `source_message`、`order_contexts`、`message_events`、订单级 Basic Information、六类 Event、S10/S99 和校验口径;后端已完成 V4 入站解析、持久化、查询、确认、复核和当前酒店数据库目录校验。 | -| `requirements/M002-v4-order-task-card-domain-model-cp2.md` | 当前有效 | M002 V4 CP2 订单任务与多卡领域模型设计,并记录 CP3-CP8 表结构、入站写入、查询、确认、复核和目录校验已落地状态;CP11 已完成 DB 目录与 lookup API,CP12 已完成前端 lookup 接入,CP13 已完成目录管理后台 CP1,CP14 已完成订单列表 V4 继续处理入口,CP15 已完成 V4 业务审计查询,CP15.1 已完成订单详情 V4 总览后端补齐,当前已停止 V4 普通业务双写旧 `workflow_reservation_task`,并已完成 Room Information 后端展示模型和前端业务化展示第一版,以及 Rooming List 确认自动 DEF 后端联动;已补 Account 范围 Rate Code、Payment 附件预览、Rooming List 事项确认卡、V4 复核态卡片交互和可编辑字段白名单文档口径;开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后置。 | -| `requirements/M002-v4-real-catalog-lookup-api-design.md` | 当前有效 | M002 V4 真实目录与 Lookup API 设计及 CP11 / CP13 CP1 实现记录,记录 Account、Market、Source、Room Type、Rate Code 从固定种子导入数据库、前端 lookup API、目录管理后端接口、权限、缓存后置、PMS / OPERA / OHIP 同步后置和失败兜底;已新增 Account + booking type 过滤 Rate Code 的下一阶段契约,房型 / 日期 / 价格规则后置。 | +| `requirements/M002-v4-order-task-card-domain-model-cp2.md` | 当前有效 | M002 V4 CP2 订单任务与多卡领域模型设计,并记录 CP3-CP8 表结构、入站写入、查询、确认、复核和目录校验已落地状态;CP11 已完成 DB 目录与 lookup API,CP12 已完成前端 lookup 接入,CP13 已完成目录管理后台 CP1,CP14 已完成订单列表 V4 继续处理入口,CP15 已完成 V4 业务审计查询,CP15.1 已完成订单详情 V4 总览后端补齐,当前已停止 V4 普通业务双写旧 `workflow_reservation_task`,并已完成 Room Information 后端展示模型和前端业务化展示第一版,以及 Rooming List 确认自动 DEF 后端联动;已补 OWNER RATE Room Type / Rate Code 目录口径、Payment 附件预览、Rooming List 事项确认卡、V4 复核态卡片交互和可编辑字段白名单文档口径;开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建,生产迁移策略后置。 | +| `requirements/M002-v4-real-catalog-lookup-api-design.md` | 当前有效 | M002 V4 真实目录与 Lookup API 设计及 CP11 / CP13 CP1 实现记录,记录 Account、Market、Source、Room Type、Rate Code 从固定种子导入数据库、前端 lookup API、目录管理后端接口、权限、缓存后置、PMS / OPERA / OHIP 同步后置和失败兜底;已记录 OWNER RATE `RATECODE (2)` 只读整理结论:Room Type 第一阶段收敛为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,Rate Code 第一阶段暂不建立 Account 适用关系,Q.B.D / LIAN TAI 清单作为酒店级目录候选。 | | `requirements/M002-v4-test-machine-smoke-checklist.md` | 当前有效 | M002 V4 测试机冒烟清单,覆盖登录、酒店权限、V4 工作台、订单任务详情、lookup、确认、复核解阻、S10/S99 ack、订单详情 V4 时间线和目录管理 CP1 排查点。 | | `requirements/M002-superagent-task-result-api-contract.md` | 阶段记录 | M002 SuperAgent 任务结果入站接口契约阶段记录;对外总契约以 `integrations/superagent-api-contract.md` 为准。 | | `requirements/M002-ai-query-minimal-fields.md` | 阶段记录 | M002 SuperAgent 查询上下文接口 1、2 最小字段落地记录;对外总契约以 `integrations/superagent-api-contract.md` 为准。 | @@ -100,6 +100,6 @@ - 接口暴露、权限、酒店隔离和审计边界以 `security-access-control-boundary.md` 为总检查清单;具体 SuperAgent / MCP / AgentBus 请求响应契约仍以 `integrations/` 下对应文档为准。 - AI-NSES 的通用标准以 `../import/reusable/ai-native-software-engineering-standard.md` 为复用来源;本项目采用方式以 `ai-native-adoption.md` 为准。 - M002 V1 只作为历史参考;V2 记录当前阶段实现;后续 M002 新开发以 `requirements/M002-order-task-workflow-v3.md` 为开发基线。 -- 2026-07-18 导入的业务基线已形成 `requirements/M002-v4-agent-callback-field-contract.md` 字段契约;M002 V4 入站解析 CP1 已落地,V4 订单任务 + 多卡领域模型设计和关键业务决策见 `requirements/M002-v4-order-task-card-domain-model-cp2.md`;M002 V4 CP3 已落地 V4 订单任务、任务卡、来源通知表结构和 Repository 基线,CP4 已落地普通 V4 业务包和 S10/S99 来源通知入站写入新模型,CP5 已落地工作台、订单任务和来源通知查询接口,CP6/CP7 已落地卡片确认、S10/S99 ack 和复核解阻,CP11 已落地 DB 目录与 lookup API,CP12 已落地前端 lookup 接入,CP13 已落地目录管理后台 CP1,CP14 已落地订单列表 V4 继续处理入口,CP15 已落地 V4 业务审计查询,CP15.1 已落地订单详情 V4 总览后端补齐且前端已接入,Room Information 后端展示模型第一版、前端业务化展示和 Rooming List 确认自动 DEF 后端联动已落地;已确认 Rooming List 卡第一版只做事项确认,`REVIEW_REQUIRED` 保持原业务卡内编辑并统一显示“确认卡片”;后续仍需 Payment 附件预览、Account 范围 Rate Code lookup / 校验、测试机联调和真实 PMS / OPERA / OHIP 同步。 +- 2026-07-18 导入的业务基线已形成 `requirements/M002-v4-agent-callback-field-contract.md` 字段契约;M002 V4 入站解析 CP1 已落地,V4 订单任务 + 多卡领域模型设计和关键业务决策见 `requirements/M002-v4-order-task-card-domain-model-cp2.md`;M002 V4 CP3 已落地 V4 订单任务、任务卡、来源通知表结构和 Repository 基线,CP4 已落地普通 V4 业务包和 S10/S99 来源通知入站写入新模型,CP5 已落地工作台、订单任务和来源通知查询接口,CP6/CP7 已落地卡片确认、S10/S99 ack 和复核解阻,CP11 已落地 DB 目录与 lookup API,CP12 已落地前端 lookup 接入,CP13 已落地目录管理后台 CP1,CP14 已落地订单列表 V4 继续处理入口,CP15 已落地 V4 业务审计查询,CP15.1 已落地订单详情 V4 总览后端补齐且前端已接入,Room Information 后端展示模型第一版、前端业务化展示和 Rooming List 确认自动 DEF 后端联动已落地;已确认 Rooming List 卡第一版只做事项确认,`REVIEW_REQUIRED` 保持原业务卡内编辑并统一显示“确认卡片”;OWNER RATE Room Type / Rate Code 目录导入口径已落地,后续仍需测试机联调和真实 PMS / OPERA / OHIP 同步。 - V3 / 旧任务前端展示和编辑字段仍以 2026-07-11 P0 冻结基线中的前端字段表、0712 字段控件说明和 `requirements/M002-task-field-control-contract-v1.md` 为白名单和控件契约基线;V4 订单任务前端展示和编辑字段以 `requirements/M002-v4-order-task-card-domain-model-cp2.md`、后端返回的 `fields[]` 和 V4 前后端协作文档为准。 - 时间点语义以 `backend-time-design.md` 为准;数据库时间点按 UTC 理解,API 返回带 `Z` 的 UTC 时间,页面再按酒店或用户时区展示。 diff --git a/docs/project/frontend-backend/backend-to-frontend-notes.md b/docs/project/frontend-backend/backend-to-frontend-notes.md index 493c9d2..2ad0b9f 100644 --- a/docs/project/frontend-backend/backend-to-frontend-notes.md +++ b/docs/project/frontend-backend/backend-to-frontend-notes.md @@ -61,8 +61,8 @@ | `GET /api/reservation/source-notifications/{notificationId}` | 查询 V4 S10/S99 来源通知详情 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,后端按来源通知实际酒店校验访问权;只返回通知摘要、来源邮件通知卡、会话摘要和 `availability`;不返回订单任务、业务卡、邮件正文、附件 URL 或原始 AI payload。 | | `GET /api/reservation/source-notifications/{notificationId}/audits` | 查询 V4 S10/S99 来源通知审计流水 | 必须带 Bearer token,需要 `RESERVATION_AUDIT_READ`,后端按来源通知实际酒店校验访问权;返回 `notification_id` 和 `items[]`。`items[]` 第一版用于展示来源通知 ack 记录,只包含脱敏后的审计摘要。 | | `GET /api/reservation/lookups/accounts` | 查询 V4 Account 目录 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,支持 `hotel_id`、`keyword`、`page_num`、`page_size`;返回统一 wrapper:`hotel_id`、`catalog_type=ACCOUNT`、`catalog_source`、`catalog_version`、`stale`、`items[]`、`page`、`warnings[]`。`keyword` 匹配目录 code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。`keyword` 无匹配时 `items=[]` / `page.total=0`,但只要酒店未过滤目录存在,`catalog_source/catalog_version` 仍保持真实目录元数据,不代表目录未初始化。前端在 `options_source=reservation_v4_account_catalog` 时调用,只提交 `items[].code`,Market / Source 以后端确认派生结果为准。 | -| `GET /api/reservation/lookups/room-types` | 查询 V4 Room Type 目录 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,支持 `hotel_id`、`keyword`、`page_num`、`page_size`;第一版只返回当前酒店 `ACTIVE` 房型目录,不接日期过滤,不代表 PMS 全量房型。`keyword` 匹配房型 code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。`keyword` 无匹配时按空选项处理,不要当作目录不可用。前端在 `options_source=reservation_v4_room_type_catalog` 时调用。 | -| `GET /api/reservation/lookups/rate-codes` | 查询 V4 Rate Code 目录 | 当前 CP11 实现只支持 `hotel_id`、`keyword`、`page_num`、`page_size`,返回当前酒店 `ACTIVE` Rate Code;下一阶段必须新增必填 `account_code`、`booking_type=GROUP/FIT`,只返回当前 Account + GROUP/FIT 适用的 Rate Code。`pricing_available=false` 表示后端未接真实价格,不要据此展示价格。`keyword` 匹配 Rate Code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。Account 合法但无适用 Rate Code 时按空选项处理,不要当作目录不可用;`account_code` 缺失 / 无效或 `booking_type` 非法时按 400 处理。前端在 `options_source=reservation_v4_rate_code_catalog` 时调用。 | +| `GET /api/reservation/lookups/room-types` | 查询 V4 Room Type 目录 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,支持 `hotel_id`、`keyword`、`page_num`、`page_size`;第一版只返回当前酒店 `ACTIVE` 房型目录,不接日期过滤,不代表 PMS 全量房型。M002-V4-owner-rate-catalog-data-alignment 后固定初始化目录已收敛为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3` 六个 code。`keyword` 匹配房型 code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。`keyword` 无匹配时按空选项处理,不要当作目录不可用。前端在 `options_source=reservation_v4_room_type_catalog` 时调用。 | +| `GET /api/reservation/lookups/rate-codes` | 查询 V4 Rate Code 目录 | 当前 CP11 实现支持 `hotel_id`、`keyword`、`page_num`、`page_size`,返回当前酒店 `ACTIVE` Rate Code;M002-V4-owner-rate-catalog-data-alignment 后固定初始化目录已收敛为 OWNER RATE `RATECODE (2)` 的 40 个规范化酒店级 code。2026-07-21 结论是第一阶段暂不建立 Account 与 Rate Code 的适用关系,Rate Code lookup 继续按酒店级目录返回,不要求 `account_code` / `booking_type`。`pricing_available=false` 表示后端未接真实价格,不要据此展示价格。`keyword` 匹配 Rate Code 时后端按稳定 code 大写归一化处理,前端可传小写;匹配显示名仍按数据库比较规则。前端在 `options_source=reservation_v4_rate_code_catalog` 时调用。 | | `GET /api/admin/reservation/catalogs/accounts` | 管理后台 Account 目录列表 | 必须带 Bearer token,需要 `RESERVATION_CATALOG_MANAGE` 和目标酒店访问权;支持 `hotel_id`、`keyword`、`status=ACTIVE/DISABLED`、`page_num`、`page_size`;`keyword` 搜索 code 时大小写不敏感,显示名仍按数据库比较规则;返回 `items[] + page`,包含 `id`、`account_code`、`account_name`、`market_code`、`source_code`、`status`、`catalog_source`、`external_account_id`、`catalog_version`、`metadata_json`、`version`、`created_at`、`updated_at`。 | | `POST /api/admin/reservation/catalogs/accounts` | 管理后台新增 Account 目录 | 必须带 `RESERVATION_CATALOG_MANAGE`;请求 `hotel_id`、`account_code`、`account_name`、`market_code`、`source_code`,可选 `external_account_id`、`catalog_version`、`metadata_json`;后端校验 Market / Source 当前酒店 ACTIVE,新增后默认 `ACTIVE`、`catalog_source=SYSTEM_MANAGED`,写管理审计。 | | `PUT /api/admin/reservation/catalogs/accounts/{accountId}/status` | 管理后台启用 / 停用 Account | 必须带 `RESERVATION_CATALOG_MANAGE`;请求 `{"status":"ACTIVE"}` 或 `{"status":"DISABLED"}`;按记录所属酒店校验访问权;停用后普通 Account lookup 不再返回;如果提交的状态和当前状态一致,后端幂等返回当前记录,不新增管理审计。 | @@ -72,7 +72,7 @@ 前端已在系统设置下新增 `/system/reservation-catalogs` 消费上述目录管理接口。页面入口要求 `RESERVATION_CATALOG_MANAGE`,列表过滤直接传 `hotel_id`、`keyword`、`status`、`page_num`、`page_size`;Account 新增第一版固定提交 `market_code=LEISURE`、`source_code=TRAVEL_AGENT`;状态重复提交按成功提示处理,不额外弹失败。 -| `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm` | 确认 V4 订单任务卡 | 必须带 Bearer token,需要 `RESERVATION_TASK_CONFIRM`,请求 JSON 带 `version`,可选 `confirmed_payload`;Basic Information 必须先确认,业务卡第一版不强制逐张顺序确认;前端只提交当前卡 `fields[]` 中可编辑字段,后端以展示快照为基准合并,未开放字段会被忽略;Room Information 卡优先提交 `confirmed_payload.room_information.final_values`,后端写入稳定确认快照并重新派生 Nights / Breakfast / Group Booking Status 文案,不写 Agent `target_order`、Adult、邮件正文、附件 URL 或前端注入字段;确认 Rooming List 卡时前端只提交 `version` 即可,若同订单为 Group 且存在可更新 Room Information 确认快照,后端会自动把 Group Booking Status 置为 `DEF` 并写审计,刷新详情时 `display_payload` / `confirmed_payload` 均显示 DEF;没有可更新投影时确认仍成功且不创建不完整 Room Information;确认前会按当前酒店数据库目录校验 Account / Room Type / Rate Code,下一阶段 `rate_code` 还必须属于已确认 Account + 当前业务 event `booking_type` 的适用范围;Room Information 新结构错误路径形如 `room_information.final_values.room_items.0.room_type_code`,历史兼容结构可能返回如 `business_fields.after.room_items.0.room_type_code`,失败返回 `V4_FIELD_VALIDATION_FAILED`;确认后卡片 `CONFIRMED`、内部写入确认快照 / 确认人 / 确认时间并锁定,重复确认返回错误;成功返回刷新后的订单任务详情。 | +| `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm` | 确认 V4 订单任务卡 | 必须带 Bearer token,需要 `RESERVATION_TASK_CONFIRM`,请求 JSON 带 `version`,可选 `confirmed_payload`;Basic Information 必须先确认,业务卡第一版不强制逐张顺序确认;前端只提交当前卡 `fields[]` 中可编辑字段,后端以展示快照为基准合并,未开放字段会被忽略;Room Information 卡优先提交 `confirmed_payload.room_information.final_values`,后端写入稳定确认快照并重新派生 Nights / Breakfast / Group Booking Status 文案,不写 Agent `target_order`、Adult、邮件正文、附件 URL 或前端注入字段;确认 Rooming List 卡时前端只提交 `version` 即可,若同订单为 Group 且存在可更新 Room Information 确认快照,后端会自动把 Group Booking Status 置为 `DEF` 并写审计,刷新详情时 `display_payload` / `confirmed_payload` 均显示 DEF;没有可更新投影时确认仍成功且不创建不完整 Room Information;确认前会按当前酒店数据库目录校验 Account / Room Type / Rate Code,Rate Code 第一阶段暂不校验 Account 适用关系;Room Information 新结构错误路径形如 `room_information.final_values.room_items.0.room_type_code`,历史兼容结构可能返回如 `business_fields.after.room_items.0.room_type_code`,失败返回 `V4_FIELD_VALIDATION_FAILED`;确认后卡片 `CONFIRMED`、内部写入确认快照 / 确认人 / 确认时间并锁定,重复确认返回错误;成功返回刷新后的订单任务详情。 | | `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution` | V4 复核解阻并确认卡片 | 必须带 Bearer token,需要 `RESERVATION_MANUAL_REVIEW_RESOLVE`,仅用于 `card_status=REVIEW_REQUIRED`;请求 JSON 带 `version`,可选 `field_overrides[]` 和 `reason`;订单任务归属未解决时 `confirmed_order_id` 必填,且必须是当前酒店下真实可见订单;`field_overrides[]` 只允许当前卡 `fields[]` 白名单内可编辑业务字段,问题字段可按 `fields[].validation_errors` 红字提示;Room Information 复核优先使用 `/room_information/final_values/...` pointer,不允许指向 `nights`、`target_order`、Adult、Block ID、Confirmation Number 等只读 / 派生字段;成功后卡片 `CONFIRMED`、`review_status=RESOLVED`,写 `review_resolution_json/confirmed_payload_json/confirmed_at/confirmed_by` 并返回刷新后的订单任务详情。 | | `POST /api/reservation/source-notifications/{notificationId}/ack` | 确认 V4 S10/S99 来源通知已读 / 已处理 | 必须带 Bearer token,需要 `RESERVATION_TASK_CONFIRM`,请求 JSON 带 `version`;仅允许 `route_code=S10/S99`;确认后 `notification_status=ACKED`,写 `ack_by/ack_at`,成功返回刷新后的来源通知详情;重复 ack 返回当前已确认状态且不新增审计;该动作不创建订单、不参与订单阻塞。 | | `GET /api/reservation/order-tasks/{orderTaskId}/audits` / `GET /api/reservation/source-notifications/{notificationId}/audits` | 查询 V4 业务审计展示数据 | 必须带 Bearer token,需要 `RESERVATION_AUDIT_READ`;前端可在 V4 订单任务详情和 S10/S99 来源通知详情的“审计时间线”中调用。响应沿用旧审计行结构:`audit_id`、`actor_type`、`actor_id`、`action`、`reason`、`before_snapshot`、`after_snapshot`、`occurred_at`。快照已由后端脱敏,前端仍不要把未知 URL-like 字符串当附件或正文直渲。 | @@ -111,7 +111,7 @@ | `GET /api/source-messages/{sourceMessageId}/conversation` | 新增邮件会话详情接口,并补齐 `html_body_sanitized` / `html_render_mode`。 | 当前唯一推荐路径是这个接口;前端渲染邮件 HTML 时优先使用 `html_body_sanitized`;不要调用历史讨论过的 `/api/source-message-conversations/{externalConversationId}`。 | | `POST /api/system/debug/eml-superagent-runs` | 新增 Debug EML 上传到 SuperAgent 调试接口,并补齐独立 Debug 外部消息 ID、原始 Message-ID 保留、安全 HTML 字段和入口通知识别。 | 只用于调试页面;请求为 multipart/form-data;必须传 `X-TH-Hotel-Debug-Upload-Key`,但该 key 不能写进前端源码、构建产物、URL、localStorage 或错误上报;SuperAgent 返回旧 S000/S999 或新 S10/S99 入口通知时都不应被前端视为 JSON 解析失败。测试机 V4 smoke 默认使用实时 AgentBus V4 subject,历史 Debug V2/V3 profile 只能由后端显式配置,不应作为 V4 smoke 入口。 | | `GET /api/reservation/workbench-items` / `/api/reservation/order-tasks/**` / `/api/reservation/source-notifications/{notificationId}` | 新增 M002 V4 CP5 查询接口,并在 CP6 打开卡片确认 / 来源通知 ack availability。 | 这是 V4 新模型前端主入口;前端应按每张卡或通知返回的 `availability.confirmable`、`availability.ackable`、`readonly_reason_code` 控制按钮。工作台条目已返回 `created_at` / `updated_at` 作为排序兜底和调试字段;前端不要继续从旧 `/api/reservation/tasks/**` 推断 V4 多卡详情。 | -| `GET /api/reservation/lookups/accounts` / `/room-types` / `/rate-codes` | 新增 M002 V4 CP11 数据库目录 lookup。 | 前端从任务详情 `fields[].options_source` 选择调用哪个 lookup;只提交返回项的 `code`,不要提交显示名、派生 Market / Source、目录完整对象或前端自造 code。Rate Code 下一阶段必须带已选 / 已确认 `account_code` 和当前业务 event `booking_type`,不能再拉全酒店全量候选。`warnings[]` 非空时可做非阻塞提示。 | +| `GET /api/reservation/lookups/accounts` / `/room-types` / `/rate-codes` | 新增 M002 V4 CP11 数据库目录 lookup。 | 前端从任务详情 `fields[].options_source` 选择调用哪个 lookup;只提交返回项的 `code`,不要提交显示名、派生 Market / Source、目录完整对象或前端自造 code。2026-07-21 结论是 Rate Code 第一阶段仍按酒店级目录 lookup,不做 Account 过滤;未来如新增适用关系,再另行扩展联动参数。`warnings[]` 非空时可做非阻塞提示。 | | `GET/POST/PUT /api/admin/reservation/catalogs/...` | 新增 M002 V4 目录管理后台 CP1 后端接口。 | 仅供系统设置 / 管理后台页面使用,必须带 `RESERVATION_CATALOG_MANAGE`;支持 Account、Room Type、Rate Code 列表、新增、启用 / 停用;停用后普通 lookup 不再返回该目录项。 | V4 CP5 分页注意:`page_num` 从 1 开始,后端第一版安全上限为 100;`page_size` 最大 100。超出上限时后端按上限处理并在 `page.page_num` / `page.page_size` 中返回实际使用值。`order_task_status` 和 `card_status` 是稳定枚举查询参数,前端不要传中文文案或自造状态码。 @@ -543,7 +543,7 @@ RESERVATION_ROOMING_LIST_GENERATE - 业务卡目录校验会递归检查稳定模型或历史兼容结构。例如 Room Information 新结构的房型位于 `/room_information/final_values/room_items/0/room_type_code`,错误详情会使用 `room_information.final_values.room_items.0.room_type_code`;历史兼容 `UPDATE_BOOKING` 的房型可能仍使用 `business_fields.after.room_items.0.room_type_code`。前端展示错误时优先用 `fields[].validation_errors`,接口 400 时可直接展示 `details[]`。 - `review-resolution` 请求示例:`{"version":0,"reason":"确认房型映射","confirmed_order_id":"123456","field_overrides":[{"field_pointer":"/room_information/final_values/room_items/0/room_type_code","value":"RM2"}]}`。`confirmed_order_id` 在订单任务归属未解决时必填;如果订单任务已经绑定订单且 `target_resolution_status=RESOLVED`,只能不传或传当前同一个订单 ID,不能借该接口切换到其它订单。`field_pointer` 必须来自当前卡 `fields[]` 中可编辑的 `basic_information.*`、`room_information.final_values.*` 或历史兼容 `business_fields.*` 叶子字段;复核态允许编辑当前卡业务白名单内字段,不再限定只能改空值、`missing_fields[]` 或目录错误字段。前端不要提交来源邮件、路由、`target_order`、`order_ref`、缺失字段清单、`manual_review`、raw evidence、校验诊断字段,也不能替换整个对象 / 数组。 - V4 `fields[]` 第一版字段说明:Basic Information 固定返回 `/basic_information/account_code`、`/basic_information/market_code`、`/basic_information/source_code`;其中 Account `control_type=select`、`options_source=reservation_v4_account_catalog`,Market / Source 为只读派生字段。Room Information 字段统一返回 `/room_information/final_values/...`,例如 `/room_information/final_values/arrival_date`、`/room_information/final_values/room_items/0/room_type_code`;`REVIEW_REQUIRED` 状态下只要字段仍在当前卡业务白名单内且未被前置阻塞,就会返回 `editable=true` 并允许 `review-resolution` 提交同一个 pointer;即使该叶子字段原始值缺失、详情页显示 `value=null`,前端仍可按原样提交该 pointer。查询侧 editable 计算和命令侧 pointer 校验共用同一套 Room Information 字段策略。测试机如仍出现 `V4_REVIEW_POINTER_NOT_ALLOWED`,确认部署后让后端日志检索 `review_pointer_policy=m002_v4_review_pointer_runtime_fix_v1`,日志会输出两侧 pointer 白名单、validation error pointers、`display_payload_has_room_information_final_values` 和拒绝原因。前端不要自行补未返回字段。 -- V4 CP11 已开放独立目录 lookup API。前端应使用 `GET /api/reservation/lookups/accounts`、`GET /api/reservation/lookups/room-types`、`GET /api/reservation/lookups/rate-codes` 渲染 Account / Room Type / Rate Code 选项;用户提交确认或复核时只提交稳定 `code`,不要提交显示名、派生 Market / Source 或目录完整对象;后端确认前仍会重新校验目录。Rate Code 下一阶段依赖 Account + `booking_type`:前端需在 Account 已选 / 已确认且能取得当前业务 event `booking_type` 后再请求 Rate Code,Account 改变后清空或重新校验已选 Rate Code;缺失条件时禁用或空态,不硬编码 OWNER RATE Excel。`keyword` 查不到只表示当前筛选无结果,不能仅凭 `items=[]` 判断目录未初始化,应结合 `catalog_source`、`catalog_version` 和 `warnings[]`。 +- V4 CP11 已开放独立目录 lookup API。前端应使用 `GET /api/reservation/lookups/accounts`、`GET /api/reservation/lookups/room-types`、`GET /api/reservation/lookups/rate-codes` 渲染 Account / Room Type / Rate Code 选项;用户提交确认或复核时只提交稳定 `code`,不要提交显示名、派生 Market / Source 或目录完整对象;后端确认前仍会重新校验目录。2026-07-21 结论是 Rate Code 第一阶段继续按当前酒店级目录查询,不依赖 Account + `booking_type` 过滤,也不硬编码 OWNER RATE Excel 中的 Account 映射;未来如新增适用关系,再由后端接口提供联动能力。`keyword` 查不到只表示当前筛选无结果,不能仅凭 `items=[]` 判断目录未初始化,应结合 `catalog_source`、`catalog_version` 和 `warnings[]`。 - M002 V4 CP12 前端已接入上述三个 lookup API:V4 多卡详情页会按当前卡 `fields[].options_source` 拉取目录选项,空 `items[]`、`stale=true` 和 `warnings[]` 作为非阻塞提示展示;Account 选择后只展示目录返回的 `market_code` / `source_code` 辅助确认,确认 / 复核请求仍只提交用户选择的 code。 - V4 新模型确认口径是不保存后端草稿、卡片最终确认后锁定、技术异常不进入用户可处理卡、当前不生成 OPERA 模拟操作。Basic Information 必须先确认;其它业务卡第一版不强制逐张顺序确认。现有 V3 `draft`、`confirm`、`manual-review-resolutions` 和 OPERA 模拟接口仍只代表旧链路能力,不能直接等同 V4 多卡最终接口。 - V4 S10/S99 已采用来源通知模型入库:新 V4 `route_code=S10/S99` 不再挂隐藏技术订单,也不再创建旧 `SOURCE_MESSAGE_ONLY` 任务;对应工作台 / 来源通知详情查询接口和 ack 写接口已开放。旧 `SOURCE_MESSAGE_ONLY` 只读任务仅代表 V3 S10/S99 和旧 S000/S999 兼容数据。 diff --git a/docs/project/frontend-backend/frontend-to-backend-api-requests.md b/docs/project/frontend-backend/frontend-to-backend-api-requests.md index 6f3fbc1..e51fa5f 100644 --- a/docs/project/frontend-backend/frontend-to-backend-api-requests.md +++ b/docs/project/frontend-backend/frontend-to-backend-api-requests.md @@ -51,7 +51,7 @@ | `GET /api/source-message-conversations/{externalConversationId}` | 未发现后端实现 | 不可以 | 历史讨论过的候选路径,当前不提供;前端统一使用 `GET /api/source-messages/{sourceMessageId}/conversation`。 | | `GET /api/reservation/message-notifications` | 未发现后端实现 | 不可以 | 历史候选路径,当前不提供。旧 S000/S999 和 V3 S10/S99 兼容数据通过 `SOURCE_MESSAGE_ONLY` 任务展示;V4 S10/S99 新数据走 V4 工作台和 `/api/reservation/source-notifications/{notificationId}`,不要再请求本候选路径。 | | `GET /api/reservation/task-card-field-whitelist` | 未发现后端实现 | 不可以 | 若任务详情 `fields[]` 已补齐 3.0 元数据,可后置。 | -| `GET /api/reservation/lookups/accounts` / `room-types` / `rate-codes` | M002 V4 CP11 已实现;Rate Code Account 范围过滤待补齐 | 可以,但 Rate Code 需后端新增过滤参数后再改前端联动 | 用于 V4 任务卡下拉 / 搜索选择;Bearer token + `RESERVATION_TASK_READ` + 酒店访问权;Account / Room Type 支持 `hotel_id`、`keyword`、`page_num`、`page_size`,只返回 ACTIVE 目录。Rate Code 下一阶段必须新增 `account_code`、`booking_type=GROUP/FIT` 必填过滤,只返回当前 Account + GROUP/FIT 适用候选。 | +| `GET /api/reservation/lookups/accounts` / `room-types` / `rate-codes` | M002 V4 CP11 已实现;M002-V4-owner-rate-catalog-data-alignment 已收敛 Room Type / Rate Code 数据;2026-07-21 结论是 Rate Code 第一阶段暂不做 Account 范围过滤 | 可以 | 用于 V4 任务卡下拉 / 搜索选择;Bearer token + `RESERVATION_TASK_READ` + 酒店访问权;Account / Room Type / Rate Code 支持 `hotel_id`、`keyword`、`page_num`、`page_size`,只返回 ACTIVE 目录。Room Type 当前固定初始化为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`;Rate Code 当前按酒店级目录返回 OWNER RATE 40 个规范化 code;未来如新增 Account 适用关系,再由后端扩展 `account_code` / `booking_type` 过滤参数。 | ## 3. 任务列表 / 工作台接口字段补齐 @@ -204,10 +204,10 @@ GET /api/reservation/orders/{orderId} "source_code": "TRAVEL_AGENT", "arrival_date": "2026-07-26", "departure_date": "2026-07-29", - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "room_items": [ { - "room_type_code": "RM1", + "room_type_code": "RM2", "room_count": 2 } ], @@ -1198,7 +1198,7 @@ POST /api/reservation/tasks/{taskId}/order-binding - `manual-review-resolutions` 成功响应中的 `opera_operations[]` 数量请后端最终确认;前端不写死两条,只按返回内容刷新展示。 - 系统管理菜单树增强接口已完成:`GET /api/admin/menus/tree`、`PUT /api/admin/menus/tree-order`。 - Manual Invoice 第一阶段的客户 / 联系人目录来源、模板初始文件、VAT 配置和生成记录是否必须落库,已在 M009 中列为开发前确认项。 -- V4 真实目录与 Lookup API 第一版已在后端 CP11 落地,前端 CP12 已接入 `GET /api/reservation/lookups/accounts`、`GET /api/reservation/lookups/room-types`、`GET /api/reservation/lookups/rate-codes` 用于 V4 字段选择控件。前端按 `options_source` 选择接口,空列表 / stale / warnings 只做非阻塞提示,确认和复核仍只提交 code;Rate Code 下一阶段已确认要按 Account + `booking_type` 过滤,前端需等后端新增 `account_code`、`booking_type` 参数和适用性校验后再联动,不能自行硬编码 OWNER RATE Excel;真实 PMS 同步、目录管理后台扩展和 SuperAgent 目录机器接口仍后置。 +- V4 真实目录与 Lookup API 第一版已在后端 CP11 落地,前端 CP12 已接入 `GET /api/reservation/lookups/accounts`、`GET /api/reservation/lookups/room-types`、`GET /api/reservation/lookups/rate-codes` 用于 V4 字段选择控件。前端按 `options_source` 选择接口,空列表 / stale / warnings 只做非阻塞提示,确认和复核仍只提交 code;2026-07-21 结论是 Rate Code 第一阶段暂不按 Account + `booking_type` 过滤,继续使用酒店级 `RATE_CODE` 目录,前端不要硬编码 OWNER RATE Excel 中的 Account 映射;真实 PMS 同步、目录管理后台扩展和 SuperAgent 目录机器接口仍后置。 - V4 Payment 附件预览下一阶段已确认:后端需补 `payment_attachments[]` 安全摘要;前端图片缩略图 + 点击大图预览,非图片文件列表 + 下载;预览和下载仍走 SourceMessage conversation 原文权限链路。`attachment_ids[]` 第一版作为 Agent 返回的只读业务事实,前端只展示并确认卡片,不做附件集合编辑。 - V4 复核态交互已确认:`REVIEW_REQUIRED` 不新建独立复核任务卡,仍在原业务卡内编辑当前卡 `fields[]` 白名单业务字段;问题字段红字提示;主按钮文案统一为“确认卡片”,但前端内部调用 `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution`。 - V4 Room Information smoke 修复已完成:`REVIEW_REQUIRED` 下当前卡白名单业务字段会按 `fields[].editable=true` 暴露给前端,`/room_information/final_values/...` pointer 可用于复核提交;Basic Information 和普通业务卡的 `display_payload` / `confirmed_payload` 不返回 Agent `target_order`,普通业务卡也会移除邮件 HTML、raw evidence、附件原始 URL 和 PMS 原始响应等敏感字段;Rooming List 自动 DEF 后刷新任务详情的 Room Information `display_payload` / `confirmed_payload` 应显示 DEF,审计接口返回 `V4_ROOMING_LIST_AUTO_DEF`;当前订单详情 `order_overview` 不返回 Group Booking Status 字段。 diff --git a/docs/project/integrations/superagent-api-contract.md b/docs/project/integrations/superagent-api-contract.md index 6f789df..c3cee5d 100644 --- a/docs/project/integrations/superagent-api-contract.md +++ b/docs/project/integrations/superagent-api-contract.md @@ -124,7 +124,7 @@ SuperAgent 不应知道或依赖内部 SourceMessage Inbox ID,也不需要为 - Basic Information 必须先确认;业务卡逐卡确认或复核解阻,确认后永久锁定;V4 第一版不提供前端草稿。 - `route_code=S10/S99` 使用 V4 来源通知模型,不创建隐藏技术订单,不进入订单详情时间线,不阻塞普通订单;前端只展示和 ack。 - Account / Market / Source、Room Type、Rate Code 以本系统数据库目录稳定代码为准;SuperAgent 不应输出显示文案作为业务判断依据。 -- Rate Code 下一阶段按当前 `order_ref` 的 `basic_information.account_code` + event `target_order.booking_type`(GROUP / FIT)限定候选;SuperAgent 仍只输出稳定 `rate_code`,不输出价格、显示名或目录对象。 +- Rate Code 第一阶段按当前酒店级 `RATE_CODE` 目录校验;2026-07-21 结论是暂不按 `basic_information.account_code` + event `target_order.booking_type`(GROUP / FIT)限定候选。SuperAgent 仍只输出稳定 `rate_code`,不输出价格、显示名或目录对象。 - Room Information 卡的 Nights、Breakfast、Group Booking Status、当前值 / 最终值 / 差异摘要由本系统后端展示模型提供;SuperAgent 不输出这些展示派生字段。 开发阶段不维护 V2/V3 旧任务兼容,测试数据可重建;该策略仅限开发 / 测试阶段,不代表生产迁移方案。生产数据迁移策略不在当前 checkpoint 处理,后续上线前另开迁移方案。 @@ -531,11 +531,11 @@ V4 普通业务包示例: }, "arrival_date": "2026-07-26", "departure_date": "2026-07-29", - "rate_code": "GRPA1", + "rate_code": "GRPA2-850UP", "booking_scenario": "STANDARD", "room_items": [ { - "room_type_code": "TWN", + "room_type_code": "RM2", "room_count": 2 } ], @@ -588,7 +588,7 @@ V4 字段说明: | `message_events[]` | 普通业务必填 | 逐 event 入站,后端按数组顺序处理。 | | `message_events[].event_type` | 是 | 第一版支持 `NEW_BOOKING`、`UPDATE_BOOKING`、`CANCEL_BOOKING`、`TRACE_RESERVATION_NOTES`、`ROOMING_LIST`、`PAYMENT`。 | | `message_events[].target_order` | 是 | `GROUP + GROUP_CODE`,或 `FIT + BOOKING_CODE / CONFIRMATION_NUMBER`。 | -| `message_events[].rate_code` | NEW_BOOKING 必填,可为 `null` | Rate Code 稳定 code;下一阶段必须属于同 `order_ref` 的 Basic Information Account + 当前 `booking_type` 的适用范围。 | +| `message_events[].rate_code` | NEW_BOOKING 必填,可为 `null` | Rate Code 稳定 code;第一阶段只要求属于当前酒店 `ACTIVE` 的 `RATE_CODE` 目录。 | | `message_events[].manual_review` | 是 | 只能是 `null` 或布尔 `true`;`true` 必须能由当前对象中的未解决字段解释。 | | Room Information 展示派生字段 | 不需要 | Nights、Breakfast、Group Booking Status、Adult、Block ID、Confirmation Number、当前订单值和差异摘要均由本系统后端查询或派生;SuperAgent 不输出。 | | New Booking 最终订单展示名 | 不需要 | Group Block Name / Fit Name 是信息系统最终订单投影字段,可由用户在 V4 任务卡中确认前编辑;Group 默认来自 `target_order.locator_value` 且 `locator_type=GROUP_CODE`,Fit 默认来自 `guest_name ?? target_order.locator_value`;SuperAgent 仍只输出 `target_order.locator_value` 作为目标定位线索,系统不得回写修改该原始定位值。 | @@ -608,7 +608,7 @@ V4 字段说明: - `PAYMENT.attachment_ids[]` 引用不存在的附件、`UPDATE_BOOKING` 携带不允许字段、目录代码无法匹配当前酒店数据库目录,以及其他 V4 event 契约错误,只写 `adapter_contract_error` transition,不创建用户可处理业务任务。 - 技术契约错误不会自动转为 S10/S99,也不会创建前端可处理业务任务。 -当前仍未完成:Payment 卡附件安全摘要和预览 / 下载联动、Account + booking type 过滤 Rate Code 的后端 lookup / 校验和前端联动、真实 OPERA / OHIP、真实 PMS / OPERA / OHIP 目录同步、普通任务切换订单、SuperAgent 目录机器接口。 +当前仍未完成:真实 OPERA / OHIP、真实 PMS / OPERA / OHIP 目录同步、普通任务切换订单、SuperAgent 目录机器接口和生产目录迁移方案。 ### 8.3 V3 S10/S99 结构化请求体 diff --git a/docs/project/requirements/M002-v4-agent-callback-field-contract.md b/docs/project/requirements/M002-v4-agent-callback-field-contract.md index 2034b64..f0ce2a9 100644 --- a/docs/project/requirements/M002-v4-agent-callback-field-contract.md +++ b/docs/project/requirements/M002-v4-agent-callback-field-contract.md @@ -16,7 +16,7 @@ 本契约用于后续 M002 V4 主流程设计、后端领域建模、前端页面模型、Adapter / MCP Schema 对齐和 SuperAgent 联调。当前后端已按本文完成 V4 入站解析和多卡模型基线:能识别 V4 包、校验关键契约、保存 AI transition / 任务卡原始 payload,并把可映射的六类 event 写入 V4 订单任务和任务卡模型。 -V4 订单任务与多卡领域模型的 CP2 设计已经单独落到 `M002-v4-order-task-card-domain-model-cp2.md`。截至 CP14 和停止旧任务双写 checkpoint,表结构、Entity、Mapper、Repository、SuperAgent V4 入站写入、V4 查询、普通卡片确认、S10/S99 ack、V4 复核解阻、数据库目录、Account / Room Type / Rate Code Lookup API、目录管理后台 CP1 后端接口、订单列表 V4 继续处理入口字段,以及 V4 普通业务不再创建旧 `workflow_reservation_task` 已实现。Room Information 卡 New / Update / Cancel 展示模型、Nights / Breakfast / Group Booking Status 派生,以及 Rooming List 确认后 Group 自动置 `DEF` 的后端联动已实现;这些均不扩大 SuperAgent 输入字段。下一阶段已确认 Rate Code 需要按订单级 Account + `booking_type`(GROUP / FIT)过滤和校验,当前实现仍是酒店级 Rate Code 目录;Payment 卡需要补付款凭证附件安全摘要和预览 / 下载联动;真实 PMS 同步仍后置,方案见 `M002-v4-real-catalog-lookup-api-design.md`。 +V4 订单任务与多卡领域模型的 CP2 设计已经单独落到 `M002-v4-order-task-card-domain-model-cp2.md`。截至 CP14 和停止旧任务双写 checkpoint,表结构、Entity、Mapper、Repository、SuperAgent V4 入站写入、V4 查询、普通卡片确认、S10/S99 ack、V4 复核解阻、数据库目录、Account / Room Type / Rate Code Lookup API、目录管理后台 CP1 后端接口、订单列表 V4 继续处理入口字段,以及 V4 普通业务不再创建旧 `workflow_reservation_task` 已实现。Room Information 卡 New / Update / Cancel 展示模型、Nights / Breakfast / Group Booking Status 派生,以及 Rooming List 确认后 Group 自动置 `DEF` 的后端联动已实现;这些均不扩大 SuperAgent 输入字段。2026-07-21 OWNER RATE `RATECODE (2)` 只读整理已确认:Room Type 第一阶段只维护 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3` 六个稳定 code,不建 Account -> Room Type 关系;Rate Code 第一阶段暂不建立 Account 适用关系,Q.B.D / LIAN TAI 的 40 个规范化 Rate Code 作为酒店级目录候选;真实 PMS 同步仍后置,方案见 `M002-v4-real-catalog-lookup-api-design.md`。 当前已确认开发阶段数据可以清空,因此 M002 V4 后续可以按新模型重建,不要求兼容旧任务数据、旧草稿、旧 OPERA 模拟、旧 `S000/S999`、旧 Fallback 或旧 `case_keys`。 @@ -199,7 +199,7 @@ Agent 给出非空 `account_code`,但信息系统运行时目录不存在该 - Market / Source:由 Account 派生,当前分别为 `LEISURE` / `TRAVEL_AGENT`。 - SuperAgent 不需要输出 `market_code`、`source_code`、`account_name`,也不要输出显示名称替代 `account_code`。 - 如果 SuperAgent 输出的 `account_code` 不在上述目录,后端会创建 `REVIEW_REQUIRED` Basic Information 卡,并在 `validation_errors_json` / 查询 `fields[].validation_errors` 中返回目录错误。 -- 如果 SuperAgent 输出的业务卡 `room_items[].room_type_code` 或 `rate_code` 不在当前酒店数据库目录,后端会创建 `REVIEW_REQUIRED` 业务卡,并在 `validation_errors_json` / 查询 `fields[].validation_errors` 中返回目录错误;下一阶段 `rate_code` 还必须属于该 `order_ref` 的 Basic Information Account + 当前 event `target_order.booking_type` 的适用关系,否则同样进入 `REVIEW_REQUIRED`。用户可通过 V4 复核解阻接口提交对应字段 pointer 修正。 +- 如果 SuperAgent 输出的业务卡 `room_items[].room_type_code` 或 `rate_code` 不在当前酒店数据库目录,后端会创建 `REVIEW_REQUIRED` 业务卡,并在 `validation_errors_json` / 查询 `fields[].validation_errors` 中返回目录错误;Rate Code 第一阶段暂不校验 Account 适用关系。用户可通过 V4 复核解阻接口提交对应字段 pointer 修正。 ## 8. message_events 公共字段 @@ -310,11 +310,11 @@ Fallback、`Need Manual Review`、独立 Voucher、旧 Payment Evidence、Vouche }, "arrival_date": "2026-07-26", "departure_date": "2026-07-29", - "rate_code": "RATE_CODE", + "rate_code": "GRPA2-850UP", "booking_scenario": "STANDARD", "room_items": [ { - "room_type_code": "TWN", + "room_type_code": "RM2", "room_count": 1 } ], @@ -336,7 +336,7 @@ Fit 条件字段: | --- | --- | --- | --- | | `arrival_date` | Group / Fit | 是,可为 `null` | 入住日期,酒店本地日期 | | `departure_date` | Group / Fit | 是,可为 `null` | 离店日期,酒店本地日期 | -| `rate_code` | Group / Fit | 是,可为 `null` | 订单级 Rate Code;下一阶段必须属于该订单 Account + `booking_type` 的适用范围 | +| `rate_code` | Group / Fit | 是,可为 `null` | 订单级 Rate Code;第一阶段只要求属于当前酒店 `ACTIVE` 的 `RATE_CODE` 目录 | | `room_items[]` | Group / Fit | 是 | 完整房型清单 | | `room_items[].room_type_code` | Group / Fit | 是,可为 `null` | 受控 RoomType code | | `room_items[].room_count` | Group / Fit | 是 | 房量,正整数 | @@ -371,7 +371,7 @@ Fit 条件字段: "departure_date": "2026-07-30", "room_items": [ { - "room_type_code": "TWN", + "room_type_code": "RM2", "room_count": 3 } ] @@ -447,7 +447,7 @@ Fit 条件字段: }, { "item_type": "EXTRA_BED", - "target_room_type_code": "TWN", + "target_room_type_code": "RM2", "extra_bed_room_count": 1, "department_code": "FO+HSK" } @@ -638,13 +638,13 @@ Account、RoomType、RateCode 的目录由信息系统或其主数据服务统 规则: - SuperAgent 只能输出目录中已有的稳定 code;Trace `department_code` 在 Department 目录未正式落地前只能输出 `FO`、`HSK`、`FO+HSK`。 -- SuperAgent 输出 `rate_code` 时必须结合当前 `order_ref` 的 `basic_information.account_code` 和 event `target_order.booking_type` 判断候选范围;本阶段只要求 Account + GROUP/FIT,不要求按房型、入住日期或价格计算。 +- SuperAgent 输出 `rate_code` 时只需要输出当前酒店目录中的稳定 code;第一阶段不要求结合当前 `order_ref` 的 `basic_information.account_code` 或 event `target_order.booking_type` 判断候选范围。 - 不允许输出自由文本或自行创造 code。 - Agent 无法可靠匹配时,按对应业务字段的未解决规则处理。 - Agent 输出非空 code、但信息系统目录不存在该值时,属于目录校验或契约问题。 - Market 和 Source 由信息系统根据订单级 `account_code` 派生,不由 Agent 输出。 -当前项目接受“SuperAgent 确定后把目录给本系统”的落地方式。第一阶段已用固定种子目录完成开发闭环;真实目录、系统管理维护、PMS / OPERA / OHIP 同步、前端 lookup API、缓存、权限和兜底策略已在 `M002-v4-real-catalog-lookup-api-design.md` 中设计。Account 范围 Rate Code 不改变 SuperAgent V4 输入结构:SuperAgent 仍只输出稳定 `rate_code`,不输出显示名、价格或目录完整对象;适用性由本系统后端目录服务最终校验。 +当前项目接受“SuperAgent 确定后把目录给本系统”的落地方式。第一阶段已用固定种子目录完成开发闭环;真实目录、系统管理维护、PMS / OPERA / OHIP 同步、前端 lookup API、缓存、权限和兜底策略已在 `M002-v4-real-catalog-lookup-api-design.md` 中设计。OWNER RATE 第一阶段暂不引入 Account 范围 Rate Code;SuperAgent 仍只输出稳定 `rate_code`,不输出显示名、价格或目录完整对象;是否存在于当前酒店目录由本系统后端目录服务最终校验。 ## 21. 后端 V4 建模建议 @@ -748,12 +748,10 @@ AI 回调包 - `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm` 确认 V4 卡片,强制 Bearer 登录、`RESERVATION_TASK_CONFIRM`、酒店访问权和 version 并发校验。 - `POST /api/reservation/source-notifications/{notificationId}/ack` 确认 V4 S10/S99 来源通知已读 / 已处理,强制 Bearer 登录、`RESERVATION_TASK_CONFIRM`、酒店访问权和 version 并发校验。 - CP7 已完成 `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution` 复核解阻和复核场景订单归属确认。 -- CP8 已完成 Account / Room Type / Rate Code 目录校验和 V4 任务卡 `fields[]` 白名单;CP11 已把固定种子导入数据库目录并开放 Account / Room Type / Rate Code lookup API;CP13 已完成目录管理后台 CP1;CP14 已在 `GET /api/reservation/orders` 补齐 V4 下一步处理入口字段。Account + booking type 过滤 Rate Code 已确认为下一阶段待实现;真实 PMS 同步仍未实现。 +- CP8 已完成 Account / Room Type / Rate Code 目录校验和 V4 任务卡 `fields[]` 白名单;CP11 已把固定种子导入数据库目录并开放 Account / Room Type / Rate Code lookup API;CP13 已完成目录管理后台 CP1;CP14 已在 `GET /api/reservation/orders` 补齐 V4 下一步处理入口字段。OWNER RATE Room Type / Rate Code 目录口径已记录,Account + Rate Code 适用关系第一阶段暂不实现;真实 PMS 同步仍未实现。 当前仍未完成: -- Account + booking type 过滤 Rate Code 的后端 lookup / 适用性校验和前端联动。 -- Payment 卡付款凭证附件安全摘要、图片缩略图 / 大图预览、非图片文件列表 + 下载联动。 - 尚未接入真实 PMS / OPERA / OHIP。 - SuperAgent 目录机器接口和生产目录同步方案仍后置。 diff --git a/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md b/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md index e1d0cf9..ee7d7cc 100644 --- a/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md +++ b/docs/project/requirements/M002-v4-order-task-card-domain-model-cp2.md @@ -16,7 +16,7 @@ M002 V4 CP1 已完成 SuperAgent V4 回调包入站解析、基础校验、路 本文是 CP2 设计文档,用于把 2026-07-18 V4 字段契约落成后续可开发的数据模型和接口草案。 -截至 CP14、V4 业务审计查询、停止旧任务双写、Room Information 后端展示模型、Rooming List 确认自动 DEF 后端联动和 Payment 附件安全摘要后端第一版,后端已实现本文第 10、11、12 节中的持久化和查询基线,并已把 SuperAgent V4 入站结果写入新表:普通业务包只创建 V4 订单任务、来源邮件展示卡、Basic Information 卡和业务卡,不再创建旧 `workflow_reservation_task`;V4 S10/S99 创建来源通知。当前已开放 V4 工作台、订单任务列表 / 详情、来源通知详情查询接口、订单详情 V4 订单任务时间线、V4 卡片确认接口、S10/S99 来源通知 ack 接口、V4 `REVIEW_REQUIRED` 卡复核解阻接口、V4 订单任务 / 来源通知审计查询接口、当前酒店数据库目录校验、卡片 `fields[]` 白名单、Account / Room Type / Rate Code lookup API、目录管理后台 CP1、订单列表 V4 继续处理入口字段、Room Information New / Update / Cancel 第一版业务展示模型、Rooming List 确认触发 Group Booking Status 自动置 `DEF`,以及 Payment 卡 `payment_attachments[]` 安全摘要。下一阶段已确认 Rate Code 需要按订单级 Account + `booking_type`(GROUP / FIT)过滤和校验,当前后端 CP11 实现仍是酒店级 Rate Code 目录,是待补齐缺口;Payment 前端仍需基于安全摘要接入缩略图 / 文件列表交互,真实预览和下载 URL 必须走 SourceMessage 原文权限链路;真实 PMS 同步继续后置,设计见 `M002-v4-real-catalog-lookup-api-design.md`。 +截至 CP14、V4 业务审计查询、停止旧任务双写、Room Information 后端展示模型、Rooming List 确认自动 DEF 后端联动和 Payment 附件安全摘要后端第一版,后端已实现本文第 10、11、12 节中的持久化和查询基线,并已把 SuperAgent V4 入站结果写入新表:普通业务包只创建 V4 订单任务、来源邮件展示卡、Basic Information 卡和业务卡,不再创建旧 `workflow_reservation_task`;V4 S10/S99 创建来源通知。当前已开放 V4 工作台、订单任务列表 / 详情、来源通知详情查询接口、订单详情 V4 订单任务时间线、V4 卡片确认接口、S10/S99 来源通知 ack 接口、V4 `REVIEW_REQUIRED` 卡复核解阻接口、V4 订单任务 / 来源通知审计查询接口、当前酒店数据库目录校验、卡片 `fields[]` 白名单、Account / Room Type / Rate Code lookup API、目录管理后台 CP1、订单列表 V4 继续处理入口字段、Room Information New / Update / Cancel 第一版业务展示模型、Rooming List 确认触发 Group Booking Status 自动置 `DEF`,以及 Payment 卡 `payment_attachments[]` 安全摘要。2026-07-21 OWNER RATE `RATECODE (2)` 只读整理已确认:Room Type 第一阶段只维护 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3` 六个稳定 code,不建 Account -> Room Type 关系;Rate Code 第一阶段暂不建立 Account 适用关系,Q.B.D / LIAN TAI 的 40 个规范化 Rate Code 作为酒店级目录候选。真实 PMS 同步继续后置,设计见 `M002-v4-real-catalog-lookup-api-design.md`。 后续如本文与 `M002-v4-agent-callback-field-contract.md` 的字段契约冲突,以字段契约为准;如与安全边界冲突,以 `security-access-control-boundary.md` 为准。 @@ -194,7 +194,7 @@ V4 新数据不再提供后端草稿保存。前端可以在页面本地维护 - 用户只能从信息系统已有 Account 目录中选择。 - 后端不得反向篡改 Agent 原始 `basic_information.manual_review`。 -CP11 起 Account / Market / Source 目录使用本系统数据库目录,不依赖 SuperAgent 动态提供目录文件。当前初始目录来自固定种子导入,`source_system=FIXED_SEED_IMPORT`;V24 会覆盖 `HOTEL-TEST`、`HOTEL-DEV` 和迁移执行时已有的 `ACTIVE` 酒店。后续 Account / Market / Source 优先由系统管理维护,Room Type / Rate Code 未来优先来自 PMS / OPERA / OHIP 同步,本地目录表和 lookup API 设计见 `M002-v4-real-catalog-lookup-api-design.md`。 +CP11 起 Account / Market / Source 目录使用本系统数据库目录,不依赖 SuperAgent 动态提供目录文件。当前初始目录来自固定种子导入,`source_system=FIXED_SEED_IMPORT`;V24 会覆盖 `HOTEL-TEST`、`HOTEL-DEV` 和迁移执行时已有的 `ACTIVE` 酒店。M002-V4-owner-rate-catalog-data-alignment 起,V25 和启动补种子已将 Room Type / Rate Code 固定种子收敛到 OWNER RATE 第一阶段目录。后续 Account / Market / Source 优先由系统管理维护,Room Type / Rate Code 未来优先来自 PMS / OPERA / OHIP 同步,本地目录表和 lookup API 设计见 `M002-v4-real-catalog-lookup-api-design.md`。 CP11 数据库初始化种子: @@ -203,10 +203,10 @@ CP11 数据库初始化种子: | Account | `QBD_TRAVEL`、`LIAN_TAI`、`HANATOUR_TD` | | Market | 由 Account 派生,当前固定为 `LEISURE` | | Source | 由 Account 派生,当前固定为 `TRAVEL_AGENT` | -| Room Type | `TWN`、`KING`、`DBL`、`SGL`、`TRP`、`RM1`、`RM2`、`RM3` | -| Rate Code | `BAR`、`RACK`、`PACKAGE`、`GROUP`、`FIT`;下一阶段候选需按 Account + `booking_type` 过滤 | +| Room Type | `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3` | +| Rate Code | OWNER RATE `RATECODE (2)` 中 Q.B.D / LIAN TAI 的 40 个规范化酒店级候选,第一阶段暂不按 Account 过滤 | -说明:Room Type / Rate Code 当前只作为确认和字段控件的第一版校验 / 选项来源代码;已开放 `GET /api/reservation/lookups/accounts|room-types|rate-codes`。Rate Code 下一阶段需引入 Account + `booking_type` 适用关系,前端不再展示全酒店 Rate Code 全量候选;房型 / 日期 / 价格过滤、真实 PMS 房型目录、Rate Code 配置中心和同步 run 后置。 +说明:Room Type / Rate Code 当前只作为确认和字段控件的第一版校验 / 选项来源代码;已开放 `GET /api/reservation/lookups/accounts|room-types|rate-codes`。Rate Code 第一阶段仍按酒店级目录 lookup;房型 / 日期 / 价格过滤、Account 适用关系、真实 PMS 房型目录、Rate Code 配置中心和同步 run 后置。 ## 8. 订单归属和 target_order @@ -759,7 +759,7 @@ POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm - 前端只应提交当前卡 `fields[]` 中可编辑字段。后端确认时以当前卡展示快照为基准合并 `confirmed_payload`,未出现在展示快照 / 字段白名单中的字段会被忽略,不会写入 `confirmed_payload_json`。 - Basic Information 确认时 `basic_information.account_code` 必须是当前酒店数据库 Account 目录值;后端确认前会派生 `account_name`、`market_code` 和 `source_code` 写入 `confirmed_payload_json`。 - Room Information 确认时,前端优先提交 `confirmed_payload.room_information.final_values` 中当前 `fields[]` 可编辑字段;后端会生成稳定确认快照 `confirmed_payload_json.room_information.final_values`。该快照不包含 Agent 原始 `target_order`,也不包含邮件正文、附件 URL、raw evidence、Adult 或前端注入字段。 -- 业务卡确认时,当前已递归校验已有 `rate_code`、`room_items[].room_type_code` 是否在当前酒店数据库目录中;下一阶段 `rate_code` 还必须属于当前订单 Basic Information 已确认 Account + 当前业务 event `booking_type` 的适用关系。Room Information 新结构的错误路径形如 `room_information.final_values.room_items.0.room_type_code`;旧兼容结构可能返回类似 `business_fields.after.room_items.0.room_type_code` 的路径,失败返回 `V4_FIELD_VALIDATION_FAILED`。 +- 业务卡确认时,当前已递归校验已有 `rate_code`、`room_items[].room_type_code` 是否在当前酒店数据库目录中;Rate Code 第一阶段暂不校验 Account 适用关系。Room Information 新结构的错误路径形如 `room_information.final_values.room_items.0.room_type_code`;旧兼容结构可能返回类似 `business_fields.after.room_items.0.room_type_code` 的路径,失败返回 `V4_FIELD_VALIDATION_FAILED`。 - 不提交草稿。 - 必须带 `version` 做并发校验。 - 后端确认后卡片 `CONFIRMED` 并锁定。 @@ -874,9 +874,9 @@ AI 原始 payload、邮件正文、附件 URL 和技术 trace 不应直接进入 | M002-V4-CP12 | 前端 Lookup 接入 | V4 卡片字段按 `options_source` 调用 lookup,替换固定种子硬编码选项,处理 stale / warning / 空目录 | | M002-V4-CP13 | 目录管理后台 V1 | Account / Market / Source 管理,临时 Room Type / Rate Code 管理,目录维护权限和管理审计 | | M002-V4-CP14 | 订单列表 V4 继续处理入口 | 已完成:`GET /api/reservation/orders` 返回 V4 下一步订单任务、卡片、动作类型、动作状态和 open 数,前端可优先跳 V4 订单任务详情 | -| M002-V4-CP14.5 | Account 范围 Rate Code Lookup | 待实现:按 Account + `booking_type` 管理和查询 Rate Code 适用关系;业务卡确认 / 复核校验 Rate Code 适用性;前端在 Account 确认后加载对应 GROUP/FIT 候选 | -| M002-V4-CP14.6 | Payment 附件预览 | 后端已完成:Payment 卡返回付款凭证附件安全摘要,不返回 URL;前端待接图片缩略图 + 大图预览、非图片文件列表 + 下载;预览 / 下载走 SourceMessage 原文权限链路 | -| M002-V4-CP14.7 | Rooming List 事项确认卡 | 待实现前端轻量展示:Rooming List 卡第一版只展示事项和确认按钮,不解析名单、不预览附件、不生成 Excel、不导入 PMS | +| M002-V4-CP14.5 | OWNER RATE 目录导入口径 | 已完成:V25 和启动补种子按人工确认结果维护 6 个 Room Type 和 Q.B.D / LIAN TAI 的 40 个酒店级 Rate Code 候选;暂不新增 Account + Rate Code 适用关系 | +| M002-V4-CP14.6 | Payment 附件预览 | 已完成前后端第一版:Payment 卡返回付款凭证附件安全摘要,不返回 URL;前端通过 SourceMessage 原文权限链路做图片缩略图 / 大图预览和非图片下载 | +| M002-V4-CP14.7 | Rooming List 事项确认卡 | 已完成前端轻量展示:Rooming List 卡第一版只展示事项和确认按钮,不解析名单、不预览附件、不生成 Excel、不导入 PMS | | M002-V4-CP14.8 | Room Information 展示模型 | 已完成前后端第一版:后端返回 `display_payload.room_information` 稳定展示模型;前端按 New / Update / Cancel 业务表单展示最终值、差异、Nights、Breakfast、Group Booking Status 和本地订单投影;Adult 不显示 | | M002-V4-CP14.9 | Rooming List 确认自动 DEF | 已完成后端第一版:确认 `ROOMING_LIST` 卡时,Group 同订单存在可更新 Room Information 确认快照则自动置 `DEF` 并写审计;Fit 不变更;无投影不造脏数据 | | M002-V4-CP14.10 | 复核态卡片字段白名单和统一确认交互 | 已完成前端第一版:`REVIEW_REQUIRED` 保持原业务卡内编辑,问题字段红字提示,前端按钮显示“确认卡片”但调用 `review-resolution`;后端 `fields[]` 返回当前卡业务字段白名单,复核写入不再只限空值或目录错误字段 | diff --git a/docs/project/requirements/M002-v4-real-catalog-lookup-api-design.md b/docs/project/requirements/M002-v4-real-catalog-lookup-api-design.md index 5ba88f9..2d65c7e 100644 --- a/docs/project/requirements/M002-v4-real-catalog-lookup-api-design.md +++ b/docs/project/requirements/M002-v4-real-catalog-lookup-api-design.md @@ -4,9 +4,9 @@ | 项目 | 内容 | | --- | --- | -| 文档版本 | 0.4 | -| 日期 | 2026-07-20 | -| 状态 | CP11 已落地第一版数据库目录与 lookup API;目录管理后台 CP1 已落地后端接口;Account + booking type 过滤 Rate Code 和 Room Information 早餐派生已确认为下一阶段待实现契约;真实 PMS 同步继续后置 | +| 文档版本 | 0.5 | +| 日期 | 2026-07-21 | +| 状态 | CP11 已落地第一版数据库目录与 lookup API;目录管理后台 CP1 已落地后端接口;OWNER RATE `RATECODE (2)` 只读整理已确认 Room Type 第一版只维护 6 个稳定 code,Rate Code 第一阶段暂不建立 Account 适用关系;Room Information 早餐派生和真实 PMS 同步继续后置 | | 适用范围 | M002 V4 Account、Market、Source、Room Type、Rate Code 目录来源、Room Information 早餐派生、数据模型、前端 lookup、缓存、酒店隔离、权限和失败兜底 | | 不适用范围 | 真实 OPERA / OHIP 写操作、真实价格计算、房型 / 日期 / 价格级 Rate Code 适用规则、前端页面实现、SuperAgent Prompt 修改、目录同步任务、SuperAgent 机器目录接口 | @@ -16,8 +16,8 @@ M002 V4 CP8 已实现第一版固定种子目录校验;M002 V4 CP11 已把该 - Basic Information 的 `account_code` 必须存在于当前酒店数据库 Account 目录。 - Market / Source 由 Account 派生,不由 SuperAgent 输出。 -- Room Type 第一版使用当前酒店数据库目录校验和字段选项提示。 -- Rate Code 当前 CP11 实现仍是酒店级目录校验和字段选项提示;下一阶段已确认需要按订单级 Account + `booking_type`(`GROUP` / `FIT`)过滤候选和校验适用性。 +- Room Type 第一版使用当前酒店数据库目录校验和字段选项提示;2026-07-21 经需求方确认,正式业务目录先收敛为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3` 六个稳定 code,不按 Account 限定房型可用范围。 +- Rate Code 当前 CP11 实现仍是酒店级目录校验和字段选项提示;2026-07-21 经需求方确认,第一阶段暂不建立 Account 与 Rate Code 的适用关系,先把 OWNER RATE `RATECODE (2)` 中已确认 Account 的 Rate Code 作为酒店级 `RATE_CODE` 目录候选维护。 - Room Information 卡中 Breakfast 第一版由后端派生:Group 固定含早;Fit 按 Rate Code 中 `RB` / `RO` 判断,无法派生时要求用户在卡片中必填确认。 - 目录错误会让对应 V4 卡片进入 `REVIEW_REQUIRED`,用户通过复核解阻选择合法 code。 @@ -34,11 +34,53 @@ CP11 之前固定种子实现位于后端 `ReservationV4DirectoryService` 和 `F | Account | Basic Information 可选目录;SuperAgent 和用户提交都使用稳定 code | `QBD_TRAVEL`、`LIAN_TAI`、`HANATOUR_TD` | 已进入数据库初始化目录,仍不是 PMS 全量 Account;后台 CP1 可新增、启用 / 停用 | | Market | 由 Account 派生的订单级 Market | 当前 Account 均派生 `LEISURE` | 已进入通用代码目录,前端仍不直接编辑 | | Source | 由 Account 派生的订单级 Source | 当前 Account 均派生 `TRAVEL_AGENT` | 已进入通用代码目录,前端仍不直接编辑 | -| Room Type | 房型 code 校验和字段选项提示 | `TWN`、`KING`、`DBL`、`SGL`、`TRP`、`RM1`、`RM2`、`RM3` | 已进入数据库初始化目录,仍不是 PMS 全量房型;后台 CP1 可新增、启用 / 停用 | -| Rate Code | Rate Code 校验和字段选项提示 | `BAR`、`RACK`、`PACKAGE`、`GROUP`、`FIT` | 已进入数据库初始化目录;当前实现仍是酒店级目录,下一阶段改为 Account + booking type 适用范围过滤;不按房型、日期或价格过滤;后台 CP1 可新增、启用 / 停用 | +| Room Type | 房型 code 校验和字段选项提示 | `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3` | 已通过 `V25__align_owner_rate_catalog_data.sql` 和启动补种子收敛为 OWNER RATE 第一阶段目录,仍不是 PMS 全量房型;后台 CP1 可新增、启用 / 停用 | +| Rate Code | Rate Code 校验和字段选项提示 | OWNER RATE `RATECODE (2)` 的 40 个规范化 Rate Code | 已通过 `V25__align_owner_rate_catalog_data.sql` 和启动补种子收敛为当前酒店级目录;第一阶段暂不建立 Account 适用关系,不按房型、日期或价格过滤;后台 CP1 可新增、启用 / 停用 | 当前固定种子只能支撑开发和演示闭环,不能作为生产长期事实源。 +## 2.1 OWNER RATE `RATECODE (2)` 目录整理结论 + +本节来自 2026-07-21 对 `/Users/andy/Downloads/OWNER RATE.xlsx` 中 `RATECODE (2)` sheet 的只读整理。M002-V4-owner-rate-catalog-data-alignment 已把本节 6 个 Room Type 和 40 个 Rate Code 写入固定初始化种子与 `V25__align_owner_rate_catalog_data.sql`;不处理 `RATECODE` sheet,不新增 Account 适用关系,不改变 lookup 接口参数契约。 + +### 2.1.1 Room Type 第一阶段稳定集合 + +需求方已确认当前系统第一阶段只维护以下 6 个 Room Type code: + +| code | 中文说明 | +| --- | --- | +| `RM2` | 高级花园景观大床房 | +| `RM3` | 高级花园景观双床房 | +| `RM4` | 高级花园景观家庭房 | +| `SU1` | 花园景观小套房(大床) | +| `SU2` | 泳池景观小套房(大床) | +| `SU3` | 家庭套房 | + +结论: + +- 第一阶段不建立 `Account -> Room Type` 关系表。 +- 所有 Account 默认都可使用上述 6 个 Room Type。 +- `RM2/RM3`、`SU1/SU2` 这类 Excel 组合值在后续导入或人工维护时应拆成多个候选 code;前端和 SuperAgent 最终提交仍只能提交单个稳定 `room_type_code`。 +- 其它 Excel 文本,例如 `Deluxe TWN`、`GLSPCB-1800`,暂不进入第一阶段 Room Type 目录,除非后续人工确认映射到上述 6 个 code 或新增正式房型。 + +### 2.1.2 Q.B.D 与 LIAN TAI Rate Code 参考清单 + +Rate Code 取 `RATECODE (2)` sheet 的 E 列原值,仅做连字符两边空格清理,例如 `GRPA2 - 850UP` 规范化为 `GRPA2-850UP`;不拆分价格、餐食、早餐地点或其它说明,不强行派生 `booking_type`。 + +第一阶段暂不建立 `Account -> Rate Code` 适用关系。下表只记录来源 Account 下出现过的 Rate Code,便于后续导入酒店级 `RATE_CODE` 目录、人工核对或未来再建适用关系。 + +| source_account_name | 当前系统 Account code 参考 | Rate Code 清单 | +| --- | --- | --- | +| `Q.B.D` | 当前固定种子为 `QBD_TRAVEL`;是否改为更短 `QBD` 待确认 | `GRPA2-850UP`、`GRPA2-1275`、`GRPA2-1400`、`GRPA2-1800`、`GRPA2-2400`、`GRPA1-900`、`GRPA1-1400`、`GRPA1-1300`、`GRPA1-1150`、`GRPA1-1725`、`GRPA1-2300`、`GRPA3-1200*B'FAST BUALUANG`、`GRPA3-2000*B'FAST BUALUANG`、`GRPA3-1400*B'FAST BUALUANG`、`GRPA3-1800*B'FAST BUALUANG`、`GRPA3-2400*B'FAST BUALUANG`、`GRPA4-1200*B'FAST LEELA`、`GRPA4-2000*B'FAST LEELA`、`GRPA4-1400*B'FAST LEELA`、`GRPA4-1800*B'FAST LEELA`、`GRPA4-2400*B'FAST LEELA` | +| `LIAN TAI` | `LIAN_TAI` | `WHO1-850UP`、`WHO1-1275`、`WHO1-1400`、`WHO1-1800`、`WHO1-2400`、`GRP1-900`、`GRP1-1400`、`GRP1-1300`、`GRP1-1800`、`GRP1-2400`、`WHO2-1100`、`WHO2-1600`、`WHO2-1400`、`WHO2-1800`、`WHO2-2400`、`WHO3-1200`、`WHO3-1800`、`WHO3-1400`、`WHO3-2400` | + +目录落地口径: + +- 当前固定初始化和 V25 已把上表 40 个去重值作为当前酒店 `workflow_reservation_catalog_code.catalog_type=RATE_CODE` 的候选目录维护,`code` 和 `display_name` 暂相同。 +- 这些值是 OWNER RATE 人工整理口径,不等同 PMS / OPERA / OHIP 的最终 Rate Plan code。 +- `B'FAST BUALUANG`、`B'FAST LEELA`、`850UP`、数字价格等内容第一阶段只作为 Rate Code 字符串的一部分保留,不单独进入价格、早餐或餐厅规则。 +- 后续如果需求方要求按 Account 限制 Rate Code,再新建适用关系表并从本节清单回填,不影响第一阶段酒店级目录校验。 + ## 3. 设计目标 真实目录能力要解决以下问题: @@ -49,7 +91,7 @@ CP11 之前固定种子实现位于后端 `ReservationV4DirectoryService` 和 `F 4. 目录有来源、版本、更新时间和启停状态,便于排查 SuperAgent 输出 code 与系统目录不一致的问题。 5. PMS / OPERA / OHIP 不稳定或暂未接入时,系统仍可使用最后一次成功目录快照或系统管理目录兜底。 6. 固定种子目录可以作为 dev/test 或导入初始化兜底,但生产不应默认靠代码固定值。 -7. Rate Code 不再作为全酒店通用下拉;业务卡 Rate Code 候选必须先受当前订单 Basic Information 的 Account 和 event `booking_type` 限定。 +7. Rate Code 第一阶段仍作为当前酒店级目录下拉;暂不按 Account、Room Type、入住日期或价格过滤。后续若需求方明确 Account 适用范围,再新增关系表和联动过滤。 ## 4. 目录来源分层 @@ -58,7 +100,7 @@ CP11 之前固定种子实现位于后端 `ReservationV4DirectoryService` 和 `F | 阶段 | 来源 | 中文说明 | 适用目录 | | --- | --- | --- | --- | | Phase 0 | `FIXED_SEED_IMPORT` | CP11 已将固定种子通过 Flyway 和启动补种子流程导入数据库,不再作为运行时代码全局 Map | 当前 Account、Market、Source、Room Type、Rate Code | -| Phase 1 | `SYSTEM_MANAGED` / `IMPORT_FILE` | 本系统数据库目录,由初始化脚本、管理后台或受控导入文件维护;`IMPORT_FILE` 用于 OWNER RATE 等人工确认过的目录 / 适用关系导入 | Account、Market、Source;也可临时维护 Room Type、Rate Code 和 Account + booking type 的 Rate Code 适用关系 | +| Phase 1 | `SYSTEM_MANAGED` / `IMPORT_FILE` | 本系统数据库目录,由初始化脚本、管理后台或受控导入文件维护;`IMPORT_FILE` 用于 OWNER RATE 等人工确认过的目录导入。当前结论是不先导入 Account 与 Rate Code 的适用关系 | Account、Market、Source;也可临时维护 Room Type、Rate Code | | Phase 2 | `PMS_SYNC` | 后端从 PMS / OPERA / OHIP 同步目录到本系统本地表,业务查询只读本地快照 | Room Type、Rate Code 优先;Account、Market、Source 视 PMS 能力再接 | 原则: @@ -75,7 +117,7 @@ CP11 之前固定种子实现位于后端 `ReservationV4DirectoryService` 和 `F | Market | 本系统管理目录 | 可选同步 PMS market code 配置 | 否,随 Account 派生展示 | 前端不直接改 Market;修改 Account 后后端派生 Market | | Source | 本系统管理目录 | 可选同步 PMS source code 配置 | 否,随 Account 派生展示 | 前端不直接改 Source;修改 Account 后后端派生 Source | | Room Type | PMS / OPERA / OHIP 同步目录优先 | 同步酒店有效房型、展示名、人数、启停状态 | 是,业务卡选择房型 | 若 PMS 未接入,可临时由系统管理维护或固定种子初始化 | -| Rate Code | PMS / OPERA / OHIP 同步目录优先;在 PMS 未接前可由系统管理或导入文件维护 Account 适用关系 | 同步有效 Rate Plan / Rate Code;Account、booking type、房型、日期和价格适用规则逐步扩展 | 是,New Booking 选择 Rate Code | 下一阶段先按 Account + GROUP/FIT 过滤候选,只选 code,不做价格计算 | +| Rate Code | PMS / OPERA / OHIP 同步目录优先;在 PMS 未接前可由系统管理或导入文件维护酒店级 Rate Code 目录 | 同步有效 Rate Plan / Rate Code;Account、booking type、房型、日期和价格适用规则后续按真实需求扩展 | 是,New Booking 选择 Rate Code | 第一阶段暂不按 Account 过滤,只选 code,不做价格计算 | Department 目录在 V4 字段契约中也会被 Trace 使用。当前任务详情页第一版先固定 `FO`、`HSK`、`FO+HSK` 三个 Department code,用于 Trace 卡下拉和 SuperAgent 输出约束;这不是正式数据库目录。后续可沿用本文模型扩展 `DEPARTMENT`、lookup API、系统管理维护和目录校验,不放入本 checkpoint。 @@ -138,11 +180,11 @@ Market、Source、Room Type、Rate Code 可先使用统一 code 表。 uk_reservation_catalog_code(hotel_id, catalog_type, code) ``` -### 6.3 推荐表:`workflow_reservation_rate_code_applicability` +### 6.3 后置备选表:`workflow_reservation_rate_code_applicability` -Rate Code 本身仍保存在 `workflow_reservation_catalog_code` 中,`catalog_type=RATE_CODE` 表示该 code 是当前酒店已知的 Rate Code;是否对某个 Account、GROUP/FIT 可用,由适用关系表表达。这样可以避免在 `metadata_json` 中写不可查询的业务规则,也避免前端硬编码 OWNER RATE Excel。 +Rate Code 本身仍保存在 `workflow_reservation_catalog_code` 中,`catalog_type=RATE_CODE` 表示该 code 是当前酒店已知的 Rate Code。2026-07-21 最新结论是第一阶段暂不建立 Account 与 Rate Code 的适用关系;普通 lookup 继续返回当前酒店 `ACTIVE` Rate Code 目录。 -本 checkpoint 只确认 Account + booking type 粒度,不纳入房型、日期和价格规则。后续如果需要按 Room Type 或入住日期进一步过滤,应在此表或独立价格规则表上扩展,不改变前端只提交稳定 `rate_code` 的基本原则。 +如果后续需求方明确“某些 Account 只能使用部分 Rate Code”,再引入本备选表表达 Account + booking type 粒度的适用关系。后续如果还需要按 Room Type 或入住日期进一步过滤,应在此表或独立价格规则表上扩展,不改变前端只提交稳定 `rate_code` 的基本原则。 | 字段 | 中文说明 | | --- | --- | @@ -228,7 +270,6 @@ workflows.reservation.service - listAccounts(...) - listRoomTypes(...) - listRateCodes(...) - - listRateCodesByAccountAndBookingType(...) integrations.ohip / integrations.pms CatalogSyncAdapter @@ -238,7 +279,7 @@ integrations.ohip / integrations.pms 规则: - V4 入站、确认、复核只调用 `ReservationV4DirectoryService`。 -- Basic Information 的 `account_code` 先确认或在同次复核中修正后,业务卡 Rate Code 才能按该 Account + `booking_type` 做适用性校验。 +- Rate Code 第一阶段只校验是否属于当前酒店 `ACTIVE` 的 `RATE_CODE` 目录;如未来引入 Account 适用关系,再基于 Basic Information 的 `account_code` 和业务 event 的 `booking_type` 扩展适用性校验。 - 前端 lookup Controller 调用 `ReservationV4CatalogLookupService`,该服务同样只读本地目录表。 - PMS / OHIP 同步 Adapter 只能写本地目录表或同步 run,不直接参与用户确认事务。 - 如果目录服务不可用,确认接口 fail closed,不接受自由文本。 @@ -370,12 +411,10 @@ GET /api/reservation/lookups/rate-codes | 参数 | 必需 | 中文说明 | | --- | --- | --- | | `hotel_id` | 否 | 当前酒店 ID | -| `account_code` | 是 | 已选择或已确认的 Reservation Account code;必须属于当前酒店 ACTIVE Account 目录 | -| `booking_type` | 是 | `GROUP` / `FIT`;取自当前业务 event 的 `target_order.booking_type` | | `keyword` | 否 | 匹配 Rate Code 或显示名 | | `page_num` / `page_size` | 否 | 分页 | -下一阶段 Rate Code lookup 必须按 `account_code + booking_type` 返回 `ACTIVE` 且适用的 Rate Code。`account_code` 未传、无效或不属于当前酒店时返回 400;`booking_type` 非 `GROUP` / `FIT` 时返回 400;Account 合法但当前无适用 Rate Code 时返回空 `items[]`。前端在 Account 未选时不应拉全酒店 Rate Code。`arrival_date` / `departure_date`、房型和价格过滤后置。 +当前 Rate Code lookup 保持酒店级目录查询,只返回当前酒店 `ACTIVE` Rate Code。2026-07-21 结论是第一阶段暂不接收 `account_code`、`booking_type` 作为必填过滤条件,也不按房型、入住日期、价格或 Account 限制候选;如未来引入 `workflow_reservation_rate_code_applicability`,再扩展查询参数和校验规则。 响应草案: @@ -383,17 +422,15 @@ GET /api/reservation/lookups/rate-codes { "hotel_id": "HOTEL-TEST", "catalog_type": "RATE_CODE", - "account_code": "QBD_TRAVEL", - "booking_type": "GROUP", - "catalog_source": "IMPORT_FILE", - "catalog_version": "owner-rate-20260720-v1", + "catalog_source": "FIXED_SEED_IMPORT", + "catalog_version": "owner-rate-20260721-v1", "stale": false, "items": [ { - "code": "GRPA1", - "display_name": "GRPA1", + "code": "GRPA2-850UP", + "display_name": "GRPA2-850UP", "status": "ACTIVE", - "catalog_source": "IMPORT_FILE", + "catalog_source": "FIXED_SEED_IMPORT", "pricing_available": false } ], @@ -409,8 +446,8 @@ GET /api/reservation/lookups/rate-codes 前端用途: - `fields[].options_source=reservation_v4_rate_code_catalog` 时调用。 -- 只能在当前订单 Basic Information 已有有效 Account,且当前业务 event 有 `booking_type` 时调用;Account 切换后必须清空或重新校验已选 Rate Code。 -- 下一阶段只选 `rate_code`,不展示或计算真实价格。 +- 当前第一阶段可以按当前酒店目录查询 Rate Code,不要求已有 Account 或 `booking_type`。 +- 第一阶段只选 `rate_code`,不展示或计算真实价格。 - V4 契约仍禁止 `UPDATE_BOOKING` 携带 Rate Code;lookup API 不改变该规则。 ### 8.4 Market / Source Lookup @@ -427,7 +464,7 @@ Market / Source 第一版不作为用户可编辑字段,不建议给普通业 | --- | --- | --- | | `reservation_v4_account_catalog` | `GET /api/reservation/lookups/accounts` | 渲染 Account 下拉 / 搜索选择,展示派生 Market / Source | | `reservation_v4_room_type_catalog` | `GET /api/reservation/lookups/room-types` | 渲染房型搜索选择 | -| `reservation_v4_rate_code_catalog` | `GET /api/reservation/lookups/rate-codes?account_code=...&booking_type=...` | 渲染当前 Account + GROUP/FIT 适用的 Rate Code 搜索选择 | +| `reservation_v4_rate_code_catalog` | `GET /api/reservation/lookups/rate-codes` | 渲染当前酒店级 Rate Code 搜索选择 | | `static_enum` | 使用 `fields[].enum_options` | 不调用 lookup | | `system_case_lookup` | 后续订单 / 任务对象 lookup | 不属于本目录 checkpoint | @@ -537,7 +574,7 @@ SuperAgent 当前不调用本 lookup API。SuperAgent 目录供给后续有两 2. 遍历每张卡 `fields[]`。 3. 只有字段 `editable=true` 且 `control_type=select/lookup` 时才加载 lookup。 4. 根据 `options_source` 选择 lookup 接口。 -5. Rate Code 字段必须先取得当前订单 Basic Information 的 Account 和当前业务 event 的 `booking_type`;缺任一条件时禁用或显示空态,不调用全酒店 Rate Code 全量查询。 +5. Rate Code 字段第一阶段按当前酒店级目录查询;如未来引入 Account 适用关系,再要求先取得当前订单 Basic Information 的 Account 和当前业务 event 的 `booking_type`。 6. 搜索输入做 debounce,不一次性拉全量。 7. 显示 `stale=true` 或 `warnings[]` 时给用户非阻塞提醒。 8. 用户提交确认或复核时只提交 code,不提交显示名、Market / Source 派生值或目录完整对象。 @@ -546,7 +583,7 @@ SuperAgent 当前不调用本 lookup API。SuperAgent 目录供给后续有两 前端禁止: - 硬编码 PMS 房型或 Rate Code 全集。 -- 硬编码 OWNER RATE Excel 中 Account 到 Rate Code 的映射;该映射必须由后端目录 / 适用关系接口提供。 +- 硬编码 OWNER RATE Excel 中 Account 到 Rate Code 的映射;当前仅允许后端目录维护酒店级 Rate Code,未来若引入适用关系也必须由后端接口提供。 - 把目录显示名当业务 code 提交。 - 绕过 `fields[]` 自行补业务字段。 - 使用 lookup API 给 SuperAgent、AgentBus 或 Debug 链路拼接输入。 @@ -560,11 +597,11 @@ SuperAgent 当前不调用本 lookup API。SuperAgent 目录供给后续有两 | M002-V4-CP12 | 前端 Lookup 接入 | V4 卡片字段渲染按 `options_source` 调用 lookup,替换固定种子硬编码选项,处理 stale / warning / 空目录 | | M002-V4-CP13 | 目录管理后台 V1 | CP1 已完成 Account / Room Type / Rate Code 前后端列表、新增、启用 / 停用闭环、`RESERVATION_CATALOG_MANAGE` 权限和管理审计;Market / Source 独立管理后置 | | M002-V4-CP14 | 订单列表 V4 继续处理入口 | 已完成:订单列表返回 V4 下一步订单任务、卡片、动作类型、动作状态和 open 数,前端可优先跳 V4 订单任务详情 | -| M002-V4-CP14.5 | Account 范围 Rate Code Lookup | 待实现:新增 Account + booking type 适用关系模型 / 导入种子,Rate Code lookup 接收 `account_code`、`booking_type`,确认和复核校验 Rate Code 适用性,前端联动 Account 后展示候选 | +| M002-V4-CP14.5 | OWNER RATE 目录导入口径 | 已完成:固定初始化种子和 V25 已把 Room Type 收敛为 `RM2`、`RM3`、`RM4`、`SU1`、`SU2`、`SU3`,并将 Q.B.D / LIAN TAI 的 40 个 Rate Code 候选作为酒店级 `RATE_CODE` 目录维护;暂不新增 Account + Rate Code 适用关系 | | M002-V4-CP15 | PMS / OPERA / OHIP 目录同步 | 同步 Adapter、同步 run 表、失败重试、最后成功快照、同步状态管理入口 | | M002-V4-CP16 | SuperAgent 目录供给 | 明确目录版本如何给 SuperAgent,必要时新增机器目录接口或导出包 | -CP11 已作为后端第一步落地,因为它不依赖真实 PMS,也能让前端后续不再硬编码当前固定种子。CP13 CP1 继续沿用本地目录表,不接真实 PMS,也不改变 SuperAgent 输入契约。 +CP11 已作为后端第一步落地,因为它不依赖真实 PMS,也能让前端后续不再硬编码当前固定种子。CP13 CP1 继续沿用本地目录表,不接真实 PMS,也不改变 SuperAgent 输入契约。CP14.5 已完成 OWNER RATE 目录数据收口,但只替换系统固定种子;测试 / 开发库如果存在人工新增的旧 Room Type / Rate Code,需要按本文第 16 节的限定 SQL 清理或重建。 ## 15. 仍需确认的问题 @@ -572,8 +609,48 @@ CP11 已作为后端第一步落地,因为它不依赖真实 PMS,也能让 2. Account code 是否继续使用本系统定义的稳定 code,例如 `QBD_TRAVEL`,还是必须对齐 PMS profile code。 3. Market / Source 是否只允许随 Account 派生,还是未来允许用户在 Basic Information 中单独改选。 4. Room Type 第一版后端已支持系统管理维护;后续是否仍要接 PMS / OHIP 同步替换为主来源待确认。 -5. Rate Code 已确认下一阶段按 `account_code + booking_type` 过滤;入住日期、房型和价格是否也要进入过滤仍待后续确认。 +5. Rate Code 第一阶段已确认暂不按 `account_code + booking_type` 过滤;如未来出现 Account 专属候选、入住日期、房型或价格过滤诉求,再单独确认是否引入适用关系或价格规则表。 6. 生产是否允许 `FIXED_SEED` 作为兜底,还是只允许 dev/test 使用。 7. SuperAgent 是否需要读取目录;如果需要,是离线给目录包,还是新增 HMAC 机器接口。 在这些问题未确认前,当前实现仍按 DB 管理目录 + 前端 lookup 查询 + 后台 CP1 手工维护闭环推进,不接真实 PMS,也不替换 SuperAgent 输入契约。 + +## 16. 测试 / 开发库 OWNER RATE 目录清理步骤 + +`V25__align_owner_rate_catalog_data.sql` 只替换 `source_system=FIXED_SEED_IMPORT` 的 Room Type / Rate Code 固定种子,不清理人工通过管理后台新增的目录项。测试机或开发库如果在 V25 前已经手工新增了旧目录值,且希望 lookup 严格只返回本阶段 6 个 Room Type 和 40 个 Rate Code,可以先备份后执行下面的限定更新。该步骤只停用当前酒店旧 Room Type / Rate Code,不清理 Account、Market、Source、用户、权限、SourceMessage、V4 order task 或 task card。 + +```sql +-- 执行前先确认目标酒店。 +SET @target_hotel_id = 'HOTEL-TEST'; + +UPDATE workflow_reservation_catalog_code +SET status = 'DISABLED', + updated_at = UTC_TIMESTAMP(6), + version = version + 1 +WHERE hotel_id = @target_hotel_id + AND catalog_type = 'ROOM_TYPE' + AND status = 'ACTIVE' + AND code NOT IN ('RM2', 'RM3', 'RM4', 'SU1', 'SU2', 'SU3'); + +UPDATE workflow_reservation_catalog_code +SET status = 'DISABLED', + updated_at = UTC_TIMESTAMP(6), + version = version + 1 +WHERE hotel_id = @target_hotel_id + AND catalog_type = 'RATE_CODE' + AND status = 'ACTIVE' + AND code NOT IN ( + 'GRPA2-850UP', 'GRPA2-1275', 'GRPA2-1400', 'GRPA2-1800', 'GRPA2-2400', + 'GRPA1-900', 'GRPA1-1400', 'GRPA1-1300', 'GRPA1-1150', 'GRPA1-1725', 'GRPA1-2300', + 'GRPA3-1200*B''FAST BUALUANG', 'GRPA3-2000*B''FAST BUALUANG', + 'GRPA3-1400*B''FAST BUALUANG', 'GRPA3-1800*B''FAST BUALUANG', + 'GRPA3-2400*B''FAST BUALUANG', + 'GRPA4-1200*B''FAST LEELA', 'GRPA4-2000*B''FAST LEELA', + 'GRPA4-1400*B''FAST LEELA', 'GRPA4-1800*B''FAST LEELA', + 'GRPA4-2400*B''FAST LEELA', + 'WHO1-850UP', 'WHO1-1275', 'WHO1-1400', 'WHO1-1800', 'WHO1-2400', + 'GRP1-900', 'GRP1-1400', 'GRP1-1300', 'GRP1-1800', 'GRP1-2400', + 'WHO2-1100', 'WHO2-1600', 'WHO2-1400', 'WHO2-1800', 'WHO2-2400', + 'WHO3-1200', 'WHO3-1800', 'WHO3-1400', 'WHO3-2400' + ); +``` diff --git a/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md b/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md index 50922e1..0e36c1d 100644 --- a/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md +++ b/docs/project/requirements/M002-v4-test-machine-smoke-checklist.md @@ -91,7 +91,7 @@ GET /api/reservation/order-tasks/{orderTaskId} ```text GET /api/reservation/lookups/accounts?hotel_id=HOTEL-TEST&keyword=QBD GET /api/reservation/lookups/room-types?hotel_id=HOTEL-TEST&keyword=RM2 -GET /api/reservation/lookups/rate-codes?hotel_id=HOTEL-TEST&keyword=GROUP +GET /api/reservation/lookups/rate-codes?hotel_id=HOTEL-TEST&keyword=GRPA2 ``` 期望: diff --git a/docs/project/security-access-control-boundary.md b/docs/project/security-access-control-boundary.md index 17003e9..b05314c 100644 --- a/docs/project/security-access-control-boundary.md +++ b/docs/project/security-access-control-boundary.md @@ -54,11 +54,11 @@ | `GET /api/reservation/source-notifications/{notificationId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 来源通知所属酒店访问权 | 只读查询默认不写业务审计;邮件正文和附件读取仍走 SourceMessage 原文权限;不得返回来源通知原始 payload 或附件 URL;前端普通通知卡如遇 URL-like 附件字符串必须二次脱敏 | | `GET /api/reservation/source-notifications/{notificationId}/audits` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_AUDIT_READ` + 来源通知所属酒店访问权 | 保持登录 + `RESERVATION_AUDIT_READ` + 来源通知所属酒店访问权;仅返回 S10/S99 来源通知 ack 审计摘要 | 查询审计不再写审计;返回快照必须脱敏,不返回原始邮件正文、HTML、附件 URL、AI 原始 payload、token 或 secret | | `GET /api/reservation/lookups/accounts` | `FRONTEND_USER` | 已实现 M002 V4 CP11;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;只返回 Account code、显示名、派生 Market / Source 和目录安全元数据 | 只读查询默认不写业务审计;不得返回 PMS 原始响应、Secret 或外部同步错误详情 | -| `GET /api/reservation/lookups/room-types` | `FRONTEND_USER` | 已实现 M002 V4 CP11;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;只返回当前酒店可选 ACTIVE 房型目录快照 | 只读查询默认不写业务审计;不得返回 PMS 原始响应、价格敏感细节或跨酒店房型 | -| `GET /api/reservation/lookups/rate-codes` | `FRONTEND_USER` | 已实现 M002 V4 CP11;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权;当前实现仍是酒店级目录 | 下一阶段保持登录 + `RESERVATION_TASK_READ` + 酒店访问权,并强制 `account_code` + `booking_type=GROUP/FIT` 过滤;只返回当前酒店、当前 Account、当前 booking type 可选 ACTIVE Rate Code 目录快照,不做真实价格计算 | 只读查询默认不写业务审计;不得返回 PMS 原始响应、价格明细、Secret、跨酒店 Rate Plan 或其他 Account 的 Rate Code 适用关系 | +| `GET /api/reservation/lookups/room-types` | `FRONTEND_USER` | 已实现 M002 V4 CP11;M002-V4-owner-rate-catalog-data-alignment 后固定初始化目录收敛为 6 个 OWNER RATE Room Type;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;只返回当前酒店可选 ACTIVE 房型目录快照 | 只读查询默认不写业务审计;不得返回 PMS 原始响应、价格敏感细节或跨酒店房型 | +| `GET /api/reservation/lookups/rate-codes` | `FRONTEND_USER` | 已实现 M002 V4 CP11;M002-V4-owner-rate-catalog-data-alignment 后固定初始化目录收敛为 OWNER RATE 40 个规范化 Rate Code;当前实现仍是酒店级目录 | 2026-07-21 结论是第一阶段继续保持登录 + `RESERVATION_TASK_READ` + 酒店访问权,并按当前酒店 `ACTIVE` Rate Code 目录返回;暂不强制 `account_code` + `booking_type=GROUP/FIT` 过滤,不做真实价格计算 | 只读查询默认不写业务审计;不得返回 PMS 原始响应、价格明细、Secret、跨酒店 Rate Plan;未来如新增 Account 适用关系,也不得泄露其他 Account 的 Rate Code 适用关系 | | `PUT /api/reservation/tasks/{taskId}/draft` | `FRONTEND_USER` | 第一版未全量强制登录;actor 仍待迁移 | 登录 + `RESERVATION_TASK_EDIT` + 酒店访问权 | 写草稿审计可按业务需要记录 | | `POST /api/reservation/tasks/{taskId}/confirm` | `FRONTEND_USER` | 第一版未全量强制登录;actor 仍待迁移 | 登录 + `RESERVATION_TASK_CONFIRM` + 酒店访问权 | 必须写业务审计 | -| `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm` | `FRONTEND_USER` | 已实现 M002 V4 CP6/CP8/CP11;强制 Bearer 登录 + `RESERVATION_TASK_CONFIRM` + 订单任务所属酒店访问权 + version 并发校验 + 当前酒店数据库目录校验 | 保持;Basic Information 前置确认,确认后卡片锁定,不返回 AI 原始 payload;Rooming List 卡确认只表示事项已人工处理,不新增名单解析、附件预览、Excel 生成或 PMS 导入权限;Payment 第一版确认只提交 `version` 和必要审计说明,不提交 `attachment_ids[]`、附件 URL 或完整附件对象;同订单为 Group 时确认 Rooming List 已自动把可更新 Room Information 确认快照中的 Group Booking Status 置为 `DEF`,没有可更新投影时不创建不完整事实;Basic Account、Room Type、Rate Code 目录错误返回 `V4_FIELD_VALIDATION_FAILED`;下一阶段 Rate Code 还必须校验属于已确认 Account + 当前业务 event `booking_type` 的适用范围 | 必须写业务审计,actor 使用当前登录用户;Rooming List 触发的 Group Booking Status 自动变更或跳过都必须记录 `V4_ROOMING_LIST_AUTO_DEF` 安全摘要 | +| `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm` | `FRONTEND_USER` | 已实现 M002 V4 CP6/CP8/CP11;强制 Bearer 登录 + `RESERVATION_TASK_CONFIRM` + 订单任务所属酒店访问权 + version 并发校验 + 当前酒店数据库目录校验 | 保持;Basic Information 前置确认,确认后卡片锁定,不返回 AI 原始 payload;Rooming List 卡确认只表示事项已人工处理,不新增名单解析、附件预览、Excel 生成或 PMS 导入权限;Payment 第一版确认只提交 `version` 和必要审计说明,不提交 `attachment_ids[]`、附件 URL 或完整附件对象;同订单为 Group 时确认 Rooming List 已自动把可更新 Room Information 确认快照中的 Group Booking Status 置为 `DEF`,没有可更新投影时不创建不完整事实;Basic Account、Room Type、Rate Code 目录错误返回 `V4_FIELD_VALIDATION_FAILED`;Rate Code 第一阶段只校验当前酒店 `ACTIVE` 目录存在,暂不校验 Account 适用关系 | 必须写业务审计,actor 使用当前登录用户;Rooming List 触发的 Group Booking Status 自动变更或跳过都必须记录 `V4_ROOMING_LIST_AUTO_DEF` 安全摘要 | | `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution` | `FRONTEND_USER` | 已实现 M002 V4 CP7/CP8/CP11;强制 Bearer 登录 + `RESERVATION_MANUAL_REVIEW_RESOLVE` + 订单任务所属酒店访问权 + version 并发校验 + 当前酒店数据库目录校验 | 保持;仅用于 V4 `REVIEW_REQUIRED` 卡,不开放普通任务任意切换订单;字段指针只允许当前卡 `fields[]` 白名单内可编辑业务字段,不允许提交来源邮件、路由、Agent 原始定位、诊断、`manual_review`、附件 URL 或 AI 原始 payload;订单归属未解决时必须提交当前酒店下真实可见订单 ID,已 `RESOLVED` 的订单任务不得换绑不同订单 | 必须写业务审计,记录复核字段指针、复核说明和订单归属确认摘要;不返回或写入 AI 原始 payload | | `POST /api/reservation/source-notifications/{notificationId}/ack` | `FRONTEND_USER` | 已实现 M002 V4 CP6;强制 Bearer 登录 + `RESERVATION_TASK_CONFIRM` + 来源通知所属酒店访问权 + version 并发校验 | 保持;只用于 `route_code=S10/S99` 的 V4 来源通知确认已读 / 已处理,不创建订单、不参与订单阻塞;重复 ack 幂等返回当前状态且不新增审计 | 首次确认必须写业务审计,记录已读 / 已处理确认,actor 使用当前登录用户 | | `POST /api/reservation/tasks/{taskId}/manual-review-conversions` | `FRONTEND_USER` | 第一版已写业务审计,但 actor 待迁移 | 登录 + `RESERVATION_MANUAL_REVIEW_RESOLVE` + 酒店访问权 | 必须写业务审计和原因 | diff --git a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationV4CatalogBootstrapRunner.java b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationV4CatalogBootstrapRunner.java index 1021faf..a3bd5fb 100644 --- a/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationV4CatalogBootstrapRunner.java +++ b/server/src/main/java/cn/nianxx/thhotel/workflows/reservation/service/impl/ReservationV4CatalogBootstrapRunner.java @@ -25,7 +25,8 @@ import org.springframework.transaction.annotation.Transactional; @Order(Ordered.HIGHEST_PRECEDENCE + 100) public class ReservationV4CatalogBootstrapRunner implements ApplicationRunner { - private static final String SEED_CATALOG_VERSION = "seed-20260719-v1"; + private static final String ACCOUNT_SEED_CATALOG_VERSION = "seed-20260719-v1"; + private static final String OWNER_RATE_CATALOG_VERSION = "owner-rate-20260721-v1"; private static final String MARKET_CODE = "LEISURE"; private static final String SOURCE_CODE = "TRAVEL_AGENT"; @@ -35,21 +36,112 @@ public class ReservationV4CatalogBootstrapRunner implements ApplicationRunner { new AccountSeed("HANATOUR_TD", "HANATOUR TD CO., LTD.")); private static final List CODE_SEEDS = List.of( - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "TWN", "TWN", 10, "{\"adult_capacity\":2}"), - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "KING", "KING", 20, "{\"adult_capacity\":2}"), - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "DBL", "DBL", 30, "{\"adult_capacity\":2}"), - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "SGL", "SGL", 40, "{\"adult_capacity\":1}"), - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "TRP", "TRP", 50, "{\"adult_capacity\":3}"), - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM1", "RM1", 60, "{\"adult_capacity\":2}"), - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM2", "RM2", 70, "{\"adult_capacity\":2}"), - new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM3", "RM3", 80, "{\"adult_capacity\":2}"), - new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "BAR", "BAR", 10, "{\"pricing_available\":false}"), - new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "RACK", "RACK", 20, "{\"pricing_available\":false}"), - new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "PACKAGE", "PACKAGE", 30, "{\"pricing_available\":false}"), - new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GROUP", "GROUP", 40, "{\"pricing_available\":false}"), - new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "FIT", "FIT", 50, "{\"pricing_available\":false}"), - new CodeSeed(ReservationV4CatalogType.MARKET.name(), MARKET_CODE, MARKET_CODE, 10, null), - new CodeSeed(ReservationV4CatalogType.SOURCE.name(), SOURCE_CODE, SOURCE_CODE, 10, null)); + new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM2", "RM2", 10, + OWNER_RATE_CATALOG_VERSION, "{\"adult_capacity\":2}"), + new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM3", "RM3", 20, + OWNER_RATE_CATALOG_VERSION, "{\"adult_capacity\":2}"), + new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "RM4", "RM4", 30, + OWNER_RATE_CATALOG_VERSION, "{\"adult_capacity\":4}"), + new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "SU1", "SU1", 40, + OWNER_RATE_CATALOG_VERSION, "{\"adult_capacity\":2}"), + new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "SU2", "SU2", 50, + OWNER_RATE_CATALOG_VERSION, "{\"adult_capacity\":2}"), + new CodeSeed(ReservationV4CatalogType.ROOM_TYPE.name(), "SU3", "SU3", 60, + OWNER_RATE_CATALOG_VERSION, "{\"adult_capacity\":4}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA2-850UP", "GRPA2-850UP", 10, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA2-1275", "GRPA2-1275", 20, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA2-1400", "GRPA2-1400", 30, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA2-1800", "GRPA2-1800", 40, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA2-2400", "GRPA2-2400", 50, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA1-900", "GRPA1-900", 60, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA1-1400", "GRPA1-1400", 70, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA1-1300", "GRPA1-1300", 80, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA1-1150", "GRPA1-1150", 90, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA1-1725", "GRPA1-1725", 100, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRPA1-2300", "GRPA1-2300", 110, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA3-1200*B'FAST BUALUANG", "GRPA3-1200*B'FAST BUALUANG", 120, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA3-2000*B'FAST BUALUANG", "GRPA3-2000*B'FAST BUALUANG", 130, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA3-1400*B'FAST BUALUANG", "GRPA3-1400*B'FAST BUALUANG", 140, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA3-1800*B'FAST BUALUANG", "GRPA3-1800*B'FAST BUALUANG", 150, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA3-2400*B'FAST BUALUANG", "GRPA3-2400*B'FAST BUALUANG", 160, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA4-1200*B'FAST LEELA", "GRPA4-1200*B'FAST LEELA", 170, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA4-2000*B'FAST LEELA", "GRPA4-2000*B'FAST LEELA", 180, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA4-1400*B'FAST LEELA", "GRPA4-1400*B'FAST LEELA", 190, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA4-1800*B'FAST LEELA", "GRPA4-1800*B'FAST LEELA", 200, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), + "GRPA4-2400*B'FAST LEELA", "GRPA4-2400*B'FAST LEELA", 210, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO1-850UP", "WHO1-850UP", 220, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO1-1275", "WHO1-1275", 230, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO1-1400", "WHO1-1400", 240, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO1-1800", "WHO1-1800", 250, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO1-2400", "WHO1-2400", 260, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRP1-900", "GRP1-900", 270, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRP1-1400", "GRP1-1400", 280, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRP1-1300", "GRP1-1300", 290, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRP1-1800", "GRP1-1800", 300, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "GRP1-2400", "GRP1-2400", 310, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO2-1100", "WHO2-1100", 320, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO2-1600", "WHO2-1600", 330, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO2-1400", "WHO2-1400", 340, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO2-1800", "WHO2-1800", 350, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO2-2400", "WHO2-2400", 360, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO3-1200", "WHO3-1200", 370, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO3-1800", "WHO3-1800", 380, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO3-1400", "WHO3-1400", 390, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.RATE_CODE.name(), "WHO3-2400", "WHO3-2400", 400, + OWNER_RATE_CATALOG_VERSION, "{\"pricing_available\":false}"), + new CodeSeed(ReservationV4CatalogType.MARKET.name(), MARKET_CODE, MARKET_CODE, 10, + ACCOUNT_SEED_CATALOG_VERSION, null), + new CodeSeed(ReservationV4CatalogType.SOURCE.name(), SOURCE_CODE, SOURCE_CODE, 10, + ACCOUNT_SEED_CATALOG_VERSION, null)); private final PlatformHotelRepository hotelRepository; private final ReservationV4CatalogRepository catalogRepository; @@ -112,7 +204,7 @@ public class ReservationV4CatalogBootstrapRunner implements ApplicationRunner { entity.setSourceCode(SOURCE_CODE); entity.setStatus(ReservationV4CatalogStatus.ACTIVE.name()); entity.setSourceSystem(ReservationV4CatalogSourceSystem.FIXED_SEED_IMPORT.name()); - entity.setCatalogVersion(SEED_CATALOG_VERSION); + entity.setCatalogVersion(ACCOUNT_SEED_CATALOG_VERSION); entity.setVersion(0L); entity.setCreatedAt(now); entity.setUpdatedAt(now); @@ -131,7 +223,7 @@ public class ReservationV4CatalogBootstrapRunner implements ApplicationRunner { entity.setStatus(ReservationV4CatalogStatus.ACTIVE.name()); entity.setSourceSystem(ReservationV4CatalogSourceSystem.FIXED_SEED_IMPORT.name()); entity.setSortOrder(seed.sortOrder()); - entity.setCatalogVersion(SEED_CATALOG_VERSION); + entity.setCatalogVersion(seed.catalogVersion()); entity.setMetadataJson(seed.metadataJson()); entity.setVersion(0L); entity.setCreatedAt(now); @@ -152,6 +244,12 @@ public class ReservationV4CatalogBootstrapRunner implements ApplicationRunner { private record AccountSeed(String accountCode, String accountName) { } - private record CodeSeed(String catalogType, String code, String displayName, int sortOrder, String metadataJson) { + private record CodeSeed( + String catalogType, + String code, + String displayName, + int sortOrder, + String catalogVersion, + String metadataJson) { } } diff --git a/server/src/main/resources/db/migration/V25__align_owner_rate_catalog_data.sql b/server/src/main/resources/db/migration/V25__align_owner_rate_catalog_data.sql new file mode 100644 index 0000000..f214f13 --- /dev/null +++ b/server/src/main/resources/db/migration/V25__align_owner_rate_catalog_data.sql @@ -0,0 +1,105 @@ +-- M002 V4 OWNER RATE 目录收口:替换系统固定 Room Type / Rate Code 种子,不清理账号、权限、SourceMessage 或 V4 业务数据。 +DELETE FROM workflow_reservation_catalog_code +WHERE catalog_type IN ('ROOM_TYPE', 'RATE_CODE') + AND source_system = 'FIXED_SEED_IMPORT' + AND ( + hotel_id IN ('HOTEL-TEST', 'HOTEL-DEV') + OR hotel_id IN ( + SELECT hotel_id + FROM platform_hotel + WHERE hotel_status = 'ACTIVE' + ) + ); + +INSERT INTO workflow_reservation_catalog_code ( + id, hotel_id, catalog_type, code, display_name, status, source_system, external_id, sort_order, + catalog_version, last_synced_at, metadata_json, version, created_at, updated_at, logic_deleted_at, + logic_deleted_reason +) +SELECT + 2500000000000000 + ROW_NUMBER() OVER (ORDER BY owner_rate_hotels.hotel_id, seed.sort_order), + owner_rate_hotels.hotel_id, + seed.catalog_type, + seed.code, + seed.display_name, + 'ACTIVE', + 'FIXED_SEED_IMPORT', + NULL, + seed.sort_order, + 'owner-rate-20260721-v1', + NULL, + seed.metadata_json, + 0, + '2026-07-21 00:00:00.000000', + '2026-07-21 00:00:00.000000', + NULL, + NULL +FROM ( + SELECT hotel_id + FROM ( + SELECT 'HOTEL-TEST' AS hotel_id + UNION ALL SELECT 'HOTEL-DEV' + UNION ALL + SELECT hotel_id + FROM platform_hotel + WHERE hotel_status = 'ACTIVE' + ) raw_hotels + WHERE hotel_id IS NOT NULL + AND hotel_id <> '' + GROUP BY hotel_id +) owner_rate_hotels +JOIN ( + SELECT 10 AS sort_order, 'ROOM_TYPE' AS catalog_type, 'RM2' AS code, 'RM2' AS display_name, '{"adult_capacity":2}' AS metadata_json + UNION ALL SELECT 20, 'ROOM_TYPE', 'RM3', 'RM3', '{"adult_capacity":2}' + UNION ALL SELECT 30, 'ROOM_TYPE', 'RM4', 'RM4', '{"adult_capacity":4}' + UNION ALL SELECT 40, 'ROOM_TYPE', 'SU1', 'SU1', '{"adult_capacity":2}' + UNION ALL SELECT 50, 'ROOM_TYPE', 'SU2', 'SU2', '{"adult_capacity":2}' + UNION ALL SELECT 60, 'ROOM_TYPE', 'SU3', 'SU3', '{"adult_capacity":4}' + UNION ALL SELECT 70, 'RATE_CODE', 'GRPA2-850UP', 'GRPA2-850UP', '{"pricing_available":false}' + UNION ALL SELECT 80, 'RATE_CODE', 'GRPA2-1275', 'GRPA2-1275', '{"pricing_available":false}' + UNION ALL SELECT 90, 'RATE_CODE', 'GRPA2-1400', 'GRPA2-1400', '{"pricing_available":false}' + UNION ALL SELECT 100, 'RATE_CODE', 'GRPA2-1800', 'GRPA2-1800', '{"pricing_available":false}' + UNION ALL SELECT 110, 'RATE_CODE', 'GRPA2-2400', 'GRPA2-2400', '{"pricing_available":false}' + UNION ALL SELECT 120, 'RATE_CODE', 'GRPA1-900', 'GRPA1-900', '{"pricing_available":false}' + UNION ALL SELECT 130, 'RATE_CODE', 'GRPA1-1400', 'GRPA1-1400', '{"pricing_available":false}' + UNION ALL SELECT 140, 'RATE_CODE', 'GRPA1-1300', 'GRPA1-1300', '{"pricing_available":false}' + UNION ALL SELECT 150, 'RATE_CODE', 'GRPA1-1150', 'GRPA1-1150', '{"pricing_available":false}' + UNION ALL SELECT 160, 'RATE_CODE', 'GRPA1-1725', 'GRPA1-1725', '{"pricing_available":false}' + UNION ALL SELECT 170, 'RATE_CODE', 'GRPA1-2300', 'GRPA1-2300', '{"pricing_available":false}' + UNION ALL SELECT 180, 'RATE_CODE', 'GRPA3-1200*B''FAST BUALUANG', 'GRPA3-1200*B''FAST BUALUANG', '{"pricing_available":false}' + UNION ALL SELECT 190, 'RATE_CODE', 'GRPA3-2000*B''FAST BUALUANG', 'GRPA3-2000*B''FAST BUALUANG', '{"pricing_available":false}' + UNION ALL SELECT 200, 'RATE_CODE', 'GRPA3-1400*B''FAST BUALUANG', 'GRPA3-1400*B''FAST BUALUANG', '{"pricing_available":false}' + UNION ALL SELECT 210, 'RATE_CODE', 'GRPA3-1800*B''FAST BUALUANG', 'GRPA3-1800*B''FAST BUALUANG', '{"pricing_available":false}' + UNION ALL SELECT 220, 'RATE_CODE', 'GRPA3-2400*B''FAST BUALUANG', 'GRPA3-2400*B''FAST BUALUANG', '{"pricing_available":false}' + UNION ALL SELECT 230, 'RATE_CODE', 'GRPA4-1200*B''FAST LEELA', 'GRPA4-1200*B''FAST LEELA', '{"pricing_available":false}' + UNION ALL SELECT 240, 'RATE_CODE', 'GRPA4-2000*B''FAST LEELA', 'GRPA4-2000*B''FAST LEELA', '{"pricing_available":false}' + UNION ALL SELECT 250, 'RATE_CODE', 'GRPA4-1400*B''FAST LEELA', 'GRPA4-1400*B''FAST LEELA', '{"pricing_available":false}' + UNION ALL SELECT 260, 'RATE_CODE', 'GRPA4-1800*B''FAST LEELA', 'GRPA4-1800*B''FAST LEELA', '{"pricing_available":false}' + UNION ALL SELECT 270, 'RATE_CODE', 'GRPA4-2400*B''FAST LEELA', 'GRPA4-2400*B''FAST LEELA', '{"pricing_available":false}' + UNION ALL SELECT 280, 'RATE_CODE', 'WHO1-850UP', 'WHO1-850UP', '{"pricing_available":false}' + UNION ALL SELECT 290, 'RATE_CODE', 'WHO1-1275', 'WHO1-1275', '{"pricing_available":false}' + UNION ALL SELECT 300, 'RATE_CODE', 'WHO1-1400', 'WHO1-1400', '{"pricing_available":false}' + UNION ALL SELECT 310, 'RATE_CODE', 'WHO1-1800', 'WHO1-1800', '{"pricing_available":false}' + UNION ALL SELECT 320, 'RATE_CODE', 'WHO1-2400', 'WHO1-2400', '{"pricing_available":false}' + UNION ALL SELECT 330, 'RATE_CODE', 'GRP1-900', 'GRP1-900', '{"pricing_available":false}' + UNION ALL SELECT 340, 'RATE_CODE', 'GRP1-1400', 'GRP1-1400', '{"pricing_available":false}' + UNION ALL SELECT 350, 'RATE_CODE', 'GRP1-1300', 'GRP1-1300', '{"pricing_available":false}' + UNION ALL SELECT 360, 'RATE_CODE', 'GRP1-1800', 'GRP1-1800', '{"pricing_available":false}' + UNION ALL SELECT 370, 'RATE_CODE', 'GRP1-2400', 'GRP1-2400', '{"pricing_available":false}' + UNION ALL SELECT 380, 'RATE_CODE', 'WHO2-1100', 'WHO2-1100', '{"pricing_available":false}' + UNION ALL SELECT 390, 'RATE_CODE', 'WHO2-1600', 'WHO2-1600', '{"pricing_available":false}' + UNION ALL SELECT 400, 'RATE_CODE', 'WHO2-1400', 'WHO2-1400', '{"pricing_available":false}' + UNION ALL SELECT 410, 'RATE_CODE', 'WHO2-1800', 'WHO2-1800', '{"pricing_available":false}' + UNION ALL SELECT 420, 'RATE_CODE', 'WHO2-2400', 'WHO2-2400', '{"pricing_available":false}' + UNION ALL SELECT 430, 'RATE_CODE', 'WHO3-1200', 'WHO3-1200', '{"pricing_available":false}' + UNION ALL SELECT 440, 'RATE_CODE', 'WHO3-1800', 'WHO3-1800', '{"pricing_available":false}' + UNION ALL SELECT 450, 'RATE_CODE', 'WHO3-1400', 'WHO3-1400', '{"pricing_available":false}' + UNION ALL SELECT 460, 'RATE_CODE', 'WHO3-2400', 'WHO3-2400', '{"pricing_available":false}' +) seed ON 1 = 1 +WHERE NOT EXISTS ( + SELECT 1 + FROM workflow_reservation_catalog_code existing + WHERE existing.hotel_id = owner_rate_hotels.hotel_id + AND existing.catalog_type = seed.catalog_type + AND existing.code = seed.code +); diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java index c944c35..800f4fb 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationFrontendQueryControllerTest.java @@ -555,10 +555,10 @@ class ReservationFrontendQueryControllerTest { "arrival_date": "2026-07-26", "departure_date": "2026-07-29", "nights": 3, - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "room_items": [ - {"room_type_code": "RM1", "room_count": 2}, - {"room_type_code": "RM2", "room_count": 1} + {"room_type_code": "RM2", "room_count": 2}, + {"room_type_code": "RM3", "room_count": 1} ] } } @@ -591,9 +591,9 @@ class ReservationFrontendQueryControllerTest { .andExpect(jsonPath("$.order_overview.source_code").value("TRAVEL_AGENT")) .andExpect(jsonPath("$.order_overview.arrival_date").value("2026-07-26")) .andExpect(jsonPath("$.order_overview.departure_date").value("2026-07-29")) - .andExpect(jsonPath("$.order_overview.rate_code").value("BAR")) + .andExpect(jsonPath("$.order_overview.rate_code").value("GRPA2-850UP")) .andExpect(jsonPath("$.order_overview.room_items.length()").value(2)) - .andExpect(jsonPath("$.order_overview.room_items[0].room_type_code").value("RM1")) + .andExpect(jsonPath("$.order_overview.room_items[0].room_type_code").value("RM2")) .andExpect(jsonPath("$.order_overview.room_items[0].room_count").value(2)) .andExpect(jsonPath("$.order_overview.room_items[?(@.room_type_code=='SHOULD_NOT_APPEAR')]") .isEmpty()) diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogBootstrapRunnerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogBootstrapRunnerTest.java index e34cabe..31d62a1 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogBootstrapRunnerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogBootstrapRunnerTest.java @@ -2,6 +2,8 @@ package cn.nianxx.thhotel.workflows.reservation.control; import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken; import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -47,5 +49,20 @@ class ReservationV4CatalogBootstrapRunnerTest { .andExpect(jsonPath("$.catalog_source").value("FIXED_SEED_IMPORT")) .andExpect(jsonPath("$.catalog_version").value("seed-20260719-v1")) .andExpect(jsonPath("$.items[0].code").value("QBD_TRAVEL")); + + performAuthorized(mockMvc, token, get("/api/reservation/lookups/room-types") + .param("hotel_id", HOTEL_ID) + .param("page_size", "20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items", hasSize(6))) + .andExpect(jsonPath("$.items[*].code", contains("RM2", "RM3", "RM4", "SU1", "SU2", "SU3"))); + + performAuthorized(mockMvc, token, get("/api/reservation/lookups/rate-codes") + .param("hotel_id", HOTEL_ID) + .param("page_size", "100")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.items", hasSize(40))) + .andExpect(jsonPath("$.items[0].code").value("GRPA2-850UP")) + .andExpect(jsonPath("$.items[39].code").value("WHO3-2400")); } } diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogLookupControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogLookupControllerTest.java index c33f6d6..05f09ab 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogLookupControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CatalogLookupControllerTest.java @@ -2,6 +2,8 @@ package cn.nianxx.thhotel.workflows.reservation.control; import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken; import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasSize; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -98,24 +100,30 @@ class ReservationV4CatalogLookupControllerTest { } @Test - void shouldReturnRoomTypeAndRateCodeLookupFromDatabaseSeed() throws Exception { + void shouldReturnOwnerRateRoomTypeAndRateCodeLookupFromDatabaseSeed() throws Exception { performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/room-types") .param("hotel_id", HOTEL_ID) - .param("keyword", "RM2")) + .param("page_size", "20")) .andExpect(status().isOk()) .andExpect(jsonPath("$.catalog_type").value("ROOM_TYPE")) .andExpect(jsonPath("$.catalog_source").value("FIXED_SEED_IMPORT")) - .andExpect(jsonPath("$.items[0].code").value("RM2")) - .andExpect(jsonPath("$.items[0].status").value("ACTIVE")); + .andExpect(jsonPath("$.items", hasSize(6))) + .andExpect(jsonPath("$.items[*].code", contains("RM2", "RM3", "RM4", "SU1", "SU2", "SU3"))) + .andExpect(jsonPath("$.page.total").value(6)); performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/rate-codes") .param("hotel_id", HOTEL_ID) - .param("keyword", "GROUP")) + .param("page_size", "100")) .andExpect(status().isOk()) .andExpect(jsonPath("$.catalog_type").value("RATE_CODE")) .andExpect(jsonPath("$.catalog_source").value("FIXED_SEED_IMPORT")) - .andExpect(jsonPath("$.items[0].code").value("GROUP")) - .andExpect(jsonPath("$.items[0].pricing_available").value(false)); + .andExpect(jsonPath("$.items", hasSize(40))) + .andExpect(jsonPath("$.items[0].code").value("GRPA2-850UP")) + .andExpect(jsonPath("$.items[20].code").value("GRPA4-2400*B'FAST LEELA")) + .andExpect(jsonPath("$.items[21].code").value("WHO1-850UP")) + .andExpect(jsonPath("$.items[39].code").value("WHO3-2400")) + .andExpect(jsonPath("$.items[0].pricing_available").value(false)) + .andExpect(jsonPath("$.page.total").value(40)); } @Test @@ -134,9 +142,9 @@ class ReservationV4CatalogLookupControllerTest { performAuthorized(mockMvc, adminToken(), get("/api/reservation/lookups/rate-codes") .param("hotel_id", HOTEL_ID) - .param("keyword", "group")) + .param("keyword", "grpa2-850up")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.items[0].code").value("GROUP")); + .andExpect(jsonPath("$.items[0].code").value("GRPA2-850UP")); } @Test diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CommandControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CommandControllerTest.java index 63c196f..1e079ce 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CommandControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4CommandControllerTest.java @@ -141,7 +141,7 @@ class ReservationV4CommandControllerTest { { "version": 0, "confirmed_payload": { - "room_items": [{"room_type_code": "TWN", "room_count": 2}] + "room_items": [{"room_type_code": "RM2", "room_count": 2}] } } """)) @@ -192,7 +192,7 @@ class ReservationV4CommandControllerTest { { "version": 0, "confirmed_payload": { - "room_items": [{"room_type_code": "TWN", "room_count": 2}], + "room_items": [{"room_type_code": "RM2", "room_count": 2}], "injected_debug_field": "SHOULD-NOT-PERSIST" } } @@ -201,7 +201,7 @@ class ReservationV4CommandControllerTest { .andExpect(jsonPath("$.order_task.order_task_status").value("COMPLETED")) .andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_items[0].room_type_code") - .value("TWN")) + .value("RM2")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.injected_debug_field").doesNotExist()) .andExpect(jsonPath("$.business_cards[0].availability.confirmable").value(false)) .andExpect(jsonPath("$.business_cards[0].availability.readonly_reason_code").value("CARD_LOCKED")); @@ -427,7 +427,7 @@ class ReservationV4CommandControllerTest { ReservationV4CardType.ROOM_INFORMATION.name(), "NEW_BOOKING", """ - {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","guest_name":"FIT Guest","room_items":[{"room_type_code":"KING","room_count":1}]} + {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","guest_name":"FIT Guest","room_items":[{"room_type_code":"RM3","room_count":1}]} """); confirmBasicCard(roomInformation); confirmRoomInformationBusinessCard(roomInformation, null); @@ -624,7 +624,7 @@ class ReservationV4CommandControllerTest { ReservationV4CardType.TRACE_RESERVATION_NOTES.name(), "TRACE_RESERVATION_NOTES", """ - {"card_type":"TRACE_RESERVATION_NOTES","event_type":"TRACE_RESERVATION_NOTES","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"TRACE_RESERVATION_NOTES","trace_items":[{"item_type":"EXTRA_BED","target_room_type_code":"TWN","extra_bed_room_count":0,"department_code":"FO+HSK"}]}} + {"card_type":"TRACE_RESERVATION_NOTES","event_type":"TRACE_RESERVATION_NOTES","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"TRACE_RESERVATION_NOTES","trace_items":[{"item_type":"EXTRA_BED","target_room_type_code":"RM2","extra_bed_room_count":0,"department_code":"FO+HSK"}]}} """); confirmBasicCard(seeded); @@ -655,7 +655,7 @@ class ReservationV4CommandControllerTest { ReservationV4CardType.TRACE_RESERVATION_NOTES.name(), "TRACE_RESERVATION_NOTES", """ - {"card_type":"TRACE_RESERVATION_NOTES","event_type":"TRACE_RESERVATION_NOTES","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"TRACE_RESERVATION_NOTES","trace_items":[{"item_type":"EXTRA_BED","target_room_type_code":"TWN","extra_bed_room_count":1.5,"department_code":"FO+HSK"}]}} + {"card_type":"TRACE_RESERVATION_NOTES","event_type":"TRACE_RESERVATION_NOTES","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"TRACE_RESERVATION_NOTES","trace_items":[{"item_type":"EXTRA_BED","target_room_type_code":"RM2","extra_bed_room_count":1.5,"department_code":"FO+HSK"}]}} """); confirmBasicCard(seeded); @@ -686,7 +686,7 @@ class ReservationV4CommandControllerTest { ReservationV4CardType.TRACE_RESERVATION_NOTES.name(), "TRACE_RESERVATION_NOTES", """ - {"card_type":"TRACE_RESERVATION_NOTES","event_type":"TRACE_RESERVATION_NOTES","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"TRACE_RESERVATION_NOTES","trace_items":[{"item_type":"EXTRA_BED","target_room_type_code":" TWN ","extra_bed_room_count":1,"department_code":" FO+HSK "}]}} + {"card_type":"TRACE_RESERVATION_NOTES","event_type":"TRACE_RESERVATION_NOTES","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"TRACE_RESERVATION_NOTES","trace_items":[{"item_type":"EXTRA_BED","target_room_type_code":" RM2 ","extra_bed_room_count":1,"department_code":" FO+HSK "}]}} """); confirmBasicCard(seeded); @@ -701,7 +701,7 @@ class ReservationV4CommandControllerTest { "trace_items": [ { "item_type": "EXTRA_BED", - "target_room_type_code": " DBL ", + "target_room_type_code": " RM3 ", "extra_bed_room_count": 2, "department_code": " FO " } @@ -711,7 +711,7 @@ class ReservationV4CommandControllerTest { """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[0].target_room_type_code") - .value("DBL")) + .value("RM3")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[0].department_code") .value("FO")); } @@ -736,7 +736,7 @@ class ReservationV4CommandControllerTest { "business_fields":{ "trace_items":[ {"item_type":"GENERAL","text":"Original trace","department_code":"FO","content":"SHOULD-NOT-PERSIST-CONTENT"}, - {"item_type":"EXTRA_BED","target_room_type_code":"TWN","extra_bed_room_count":1,"department_code":"FO+HSK","content":"SHOULD-NOT-PERSIST-EXTRA-CONTENT","attachment_url":"https://oss.example.test/trace-confirm-secret.png"} + {"item_type":"EXTRA_BED","target_room_type_code":"RM2","extra_bed_room_count":1,"department_code":"FO+HSK","content":"SHOULD-NOT-PERSIST-EXTRA-CONTENT","attachment_url":"https://oss.example.test/trace-confirm-secret.png"} ] } } @@ -761,7 +761,7 @@ class ReservationV4CommandControllerTest { }, { "item_type": "EXTRA_BED", - "target_room_type_code": "DBL", + "target_room_type_code": "RM3", "extra_bed_room_count": 2, "department_code": "FO", "content": "SHOULD-NOT-PERSIST-SUBMITTED-EXTRA-CONTENT", @@ -777,7 +777,7 @@ class ReservationV4CommandControllerTest { .andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[0].department_code") .value("HSK")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[1].target_room_type_code") - .value("DBL")) + .value("RM3")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[1].extra_bed_room_count") .value(2)) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.trace_items[1].department_code") @@ -798,7 +798,7 @@ class ReservationV4CommandControllerTest { Instant.parse("2026-07-19T01:13:10Z"), null, """ - {"card_type":"ROOM_INFORMATION","event_type":"UPDATE_BOOKING","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"UPDATE_BOOKING","after":{"room_items":[{"room_type_code":"TWN","room_count":2}]}}} + {"card_type":"ROOM_INFORMATION","event_type":"UPDATE_BOOKING","business_fields":{"order_ref":"ORDER-COMMAND","event_type":"UPDATE_BOOKING","after":{"room_items":[{"room_type_code":"RM2","room_count":2}]}}} """); confirmBasicCard(seeded); @@ -816,7 +816,7 @@ class ReservationV4CommandControllerTest { "after": { "room_items": [ { - "room_type_code": "DBL", + "room_type_code": "RM3", "room_count": 1, "injected_debug_field": "SHOULD-NOT-PERSIST" } @@ -833,7 +833,7 @@ class ReservationV4CommandControllerTest { .andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields.event_type") .value("UPDATE_BOOKING")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields.after.room_items[0].room_type_code") - .value("DBL")) + .value("RM3")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields.after.room_items[0].room_count") .value(1)) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields.after.room_items[0].injected_debug_field") @@ -861,8 +861,8 @@ class ReservationV4CommandControllerTest { "pms_raw_response":{"debug":"NESTED-COMMAND-SHOULD-NOT-PERSIST"}, "arrival_date":"2026-07-26", "departure_date":"2026-07-29", - "rate_code":"GROUP", - "room_items":[{"room_type_code":"TWN","room_count":2,"raw_evidence":"NESTED-ROOM-RAW","pms_room_type_code":"NESTED-PMS-SHOULD-NOT-PERSIST"}] + "rate_code":"GRPA2-850UP", + "room_items":[{"room_type_code":"RM2","room_count":2,"raw_evidence":"NESTED-ROOM-RAW","pms_room_type_code":"NESTED-PMS-SHOULD-NOT-PERSIST"}] } } """); @@ -882,12 +882,12 @@ class ReservationV4CommandControllerTest { "arrival_date": "2026-07-27", "departure_date": "2026-07-30", "nights": 99, - "rate_code": "GROUP", + "rate_code": "GRPA2-850UP", "breakfast_included": false, "group_booking_status": "DEF", "room_items": [ { - "room_type_code": "DBL", + "room_type_code": "RM3", "room_count": 3, "pms_room_type_code": "SHOULD-NOT-PERSIST" } @@ -915,7 +915,7 @@ class ReservationV4CommandControllerTest { .andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.group_booking_status") .value("DEF")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.room_items[0].room_type_code") - .value("DBL")) + .value("RM3")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.room_items[0].room_count") .value(3)) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.room_items[0].pms_room_type_code") @@ -959,8 +959,8 @@ class ReservationV4CommandControllerTest { "event_type":"NEW_BOOKING", "arrival_date":"2026-07-26", "departure_date":"2026-07-29", - "rate_code":"GROUP", - "room_items":[{"room_type_code":"TWN","room_count":2}] + "rate_code":"GRPA2-850UP", + "room_items":[{"room_type_code":"RM2","room_count":2}] } } """); @@ -978,10 +978,10 @@ class ReservationV4CommandControllerTest { "final_values": { "arrival_date": "2026-07-30", "departure_date": "2026-07-29", - "rate_code": "GROUP", + "rate_code": "GRPA2-850UP", "group_block_name": "GRP-V4-COMMAND-RI-DATE-001", "room_items": [ - {"room_type_code": "TWN", "room_count": 2} + {"room_type_code": "RM2", "room_count": 2} ] } } @@ -1011,8 +1011,8 @@ class ReservationV4CommandControllerTest { "guest_name":"LI CHUNHONG", "arrival_date":"2026-07-26", "departure_date":"2026-07-29", - "rate_code":"FIT", - "room_items":[{"room_type_code":"KING","room_count":1}] + "rate_code":"WHO1-850UP", + "room_items":[{"room_type_code":"RM3","room_count":1}] } } """); @@ -1030,9 +1030,9 @@ class ReservationV4CommandControllerTest { "final_values": { "arrival_date": "2026-07-26", "departure_date": "2026-07-29", - "rate_code": "FIT", + "rate_code": "WHO1-850UP", "room_items": [ - {"room_type_code": "KING", "room_count": 1} + {"room_type_code": "RM3", "room_count": 1} ] } } @@ -1247,7 +1247,7 @@ class ReservationV4CommandControllerTest { "field_overrides": [ { "field_pointer": "/business_fields/room_items/0/room_type_code", - "value": "TWN" + "value": "RM2" } ] } @@ -1255,7 +1255,7 @@ class ReservationV4CommandControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields.room_items[0].room_type_code") - .value("TWN")) + .value("RM2")) .andExpect(jsonPath("$.business_cards[0].review_resolution.reason").value("修正房型目录")); } @@ -1277,7 +1277,7 @@ class ReservationV4CommandControllerTest { "event_type":"NEW_BOOKING", "arrival_date":"2026-08-01", "departure_date":"2026-08-03", - "rate_code":"GROUP", + "rate_code":"GRPA2-850UP", "room_items":[{"room_type_code":"UNKNOWN_TYPE","room_count":2}] } } @@ -1636,7 +1636,7 @@ class ReservationV4CommandControllerTest { ArrayNode overrides = request.putArray("field_overrides"); ObjectNode override = overrides.addObject(); override.put("field_pointer", editableField.path("field_pointer").asText()); - override.put("value", "TWN"); + override.put("value", "RM2"); performAuthorized(mockMvc, adminToken(), post( "/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution", @@ -1647,7 +1647,7 @@ class ReservationV4CommandControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.business_cards[0].card_status").value("CONFIRMED")) .andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.room_items[0].room_type_code") - .value("TWN")); + .value("RM2")); } @Test @@ -1736,8 +1736,8 @@ class ReservationV4CommandControllerTest { "event_type":"NEW_BOOKING", "arrival_date":"2026-08-01", "departure_date":"2026-08-03", - "rate_code":"GROUP", - "room_items":[{"room_type_code":"TWN","room_count":2}] + "rate_code":"GRPA2-850UP", + "room_items":[{"room_type_code":"RM2","room_count":2}] } } """, @@ -1810,7 +1810,7 @@ class ReservationV4CommandControllerTest { "arrival_date":"2026-08-01", "departure_date":"2026-08-03", "rate_code":"FIT-RB", - "room_items":[{"room_type_code":"TWN","room_count":1}] + "room_items":[{"room_type_code":"RM2","room_count":1}] } } """, @@ -2448,10 +2448,10 @@ class ReservationV4CommandControllerTest { "final_values": { "arrival_date": "2026-07-26", "departure_date": "2026-07-29", - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "fit_name": "FIT Guest", "breakfast_included": true, - "room_items": [{"room_type_code": "TWN", "room_count": 2}]%s + "room_items": [{"room_type_code": "RM2", "room_count": 2}]%s } } } @@ -2512,7 +2512,7 @@ class ReservationV4CommandControllerTest { Instant receivedAt, Long orderId) { return seedOrderTaskWithBusinessCard(hotelId, externalMessageId, receivedAt, orderId, """ - {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"TWN","room_count":2}]} + {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"RM2","room_count":2}]} """); } @@ -2641,7 +2641,7 @@ class ReservationV4CommandControllerTest { basicStatus, businessStatus, """ - {"event_type":"NEW_BOOKING","route_code":"S01","business_fields":{"order_ref":"ORDER-REVIEW","event_type":"NEW_BOOKING","manual_review":true,"room_items":[{"room_type_code":"TWN","room_count":2,"pms_room_type_code":null}]}} + {"event_type":"NEW_BOOKING","route_code":"S01","business_fields":{"order_ref":"ORDER-REVIEW","event_type":"NEW_BOOKING","manual_review":true,"room_items":[{"room_type_code":"RM2","room_count":2,"pms_room_type_code":null}]}} """, null); } diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4QueryControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4QueryControllerTest.java index 95251e2..30a8fe1 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4QueryControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/ReservationV4QueryControllerTest.java @@ -295,8 +295,8 @@ class ReservationV4QueryControllerTest { "pms_raw_response":{"debug":"NESTED-SHOULD-NOT-LEAK"}, "arrival_date":"2026-07-26", "departure_date":"2026-07-29", - "rate_code":"GROUP", - "room_items":[{"room_type_code":"TWN","room_count":2,"pms_room_type_code":"NESTED-PMS-SHOULD-NOT-LEAK","raw_evidence":"NESTED-ROOM-RAW"}] + "rate_code":"GRPA2-850UP", + "room_items":[{"room_type_code":"RM2","room_count":2,"pms_room_type_code":"NESTED-PMS-SHOULD-NOT-LEAK","raw_evidence":"NESTED-ROOM-RAW"}] } } """, @@ -318,7 +318,7 @@ class ReservationV4QueryControllerTest { .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.nights") .value(3)) .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.rate_code") - .value("GROUP")) + .value("GRPA2-850UP")) .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.breakfast_included") .value(true)) .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.group_booking_status") @@ -371,10 +371,10 @@ class ReservationV4QueryControllerTest { "arrival_date":"2026-07-26", "departure_date":"2026-07-29", "nights":3, - "rate_code":"GROUP", + "rate_code":"GRPA2-850UP", "breakfast_included":true, "group_booking_status":"TEN", - "room_items":[{"room_type_code":"TWN","room_count":2}] + "room_items":[{"room_type_code":"RM2","room_count":2}] } } } @@ -398,8 +398,8 @@ class ReservationV4QueryControllerTest { "after":{ "arrival_date":"2026-07-27", "departure_date":"2026-07-31", - "rate_code":"BAR", - "room_items":[{"room_type_code":"DBL","room_count":3}] + "rate_code":"GRPA2-850UP", + "room_items":[{"room_type_code":"RM3","room_count":3}] } } } @@ -420,7 +420,7 @@ class ReservationV4QueryControllerTest { .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.nights") .value(4)) .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.rate_code") - .value("GROUP")) + .value("GRPA2-850UP")) .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.change_summary[?(@.field=='rate_code')]") .doesNotExist()) .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.change_summary[?(@.field=='nights')].before") @@ -452,7 +452,7 @@ class ReservationV4QueryControllerTest { "event_type":"NEW_BOOKING", "arrival_date":"2026-08-01", "departure_date":"2026-08-03", - "rate_code":"GROUP", + "rate_code":"GRPA2-850UP", "room_items":[{"room_type_code":"UNKNOWN_TYPE","room_count":2}] } } @@ -498,7 +498,7 @@ class ReservationV4QueryControllerTest { ReservationV4CardStatus.PENDING_CONFIRM.name(), ReservationV4CardStatus.PENDING_CONFIRM.name(), """ - {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"TWN","room_count":2}]} + {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"RM2","room_count":2}]} """, null); ReservationV4TaskCardSnapshot basicCard = workflowRepository.findTaskCardsByOrderTaskId(HOTEL_ID, orderTask.id()) @@ -819,7 +819,7 @@ class ReservationV4QueryControllerTest { { "item_type":"EXTRA_BED", "content":"SHOULD-NOT-LEAK-EXTRA-BED-CONTENT", - "target_room_type_code":"TWN", + "target_room_type_code":"RM2", "extra_bed_room_count":1, "department_code":"FO+HSK", "raw_evidence":"SHOULD-NOT-LEAK-RAW", @@ -837,7 +837,7 @@ class ReservationV4QueryControllerTest { .andExpect(jsonPath("$.business_cards[0].display_payload.trace_items[0].text") .value("Guest requests quiet rooms.")) .andExpect(jsonPath("$.business_cards[0].display_payload.trace_items[1].target_room_type_code") - .value("TWN")) + .value("RM2")) .andExpect(jsonPath("$.business_cards[0].display_payload.trace_items[1].extra_bed_room_count") .value(1)) .andExpect(jsonPath("$.business_cards[0].display_payload.business_fields").doesNotExist()) @@ -875,10 +875,10 @@ class ReservationV4QueryControllerTest { "arrival_date":"2026-08-01", "departure_date":"2026-08-05", "nights":4, - "rate_code":"GROUP", + "rate_code":"GRPA2-850UP", "breakfast_included":true, "group_booking_status":"DEF", - "room_items":[{"room_type_code":"KING","room_count":1}] + "room_items":[{"room_type_code":"RM3","room_count":1}] } } } @@ -910,7 +910,7 @@ class ReservationV4QueryControllerTest { .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.current_values.arrival_date") .value("2026-08-01")) .andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.room_items[0].room_type_code") - .value("KING")) + .value("RM3")) .andExpect(jsonPath("$.business_cards[0].fields.length()").value(0)) .andExpect(jsonPath("$.business_cards[0].availability.confirmable").value(true)); } @@ -1248,7 +1248,7 @@ class ReservationV4QueryControllerTest { ReservationV4CardStatus.PENDING_CONFIRM.name(), ReservationV4CardStatus.PENDING_CONFIRM.name(), """ - {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"TWN","room_count":2}]} + {"card_type":"ROOM_INFORMATION","event_type":"NEW_BOOKING","room_items":[{"room_type_code":"RM2","room_count":2}]} """, null); } diff --git a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java index 362dfce..31e835f 100644 --- a/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java +++ b/server/src/test/java/cn/nianxx/thhotel/workflows/reservation/control/SuperAgentTaskResultControllerTest.java @@ -1314,7 +1314,7 @@ class SuperAgentTaskResultControllerTest { void shouldMarkV4BusinessCardReviewRequiredWhenRoomTypeCodeUnknown() throws Exception { SourceMessageCaptureResult source = captureSourceMessage("mail-v4-unknown-room-type-001"); String body = v4BusinessRootBody("mail-v4-unknown-room-type-001") - .replace("\"room_type_code\": \"TWN\"", "\"room_type_code\": \"UNKNOWN_ROOM\"") + .replace("\"room_type_code\": \"RM2\"", "\"room_type_code\": \"UNKNOWN_ROOM\"") .replace("GRP-V4-001", "GRP-V4-UNKNOWN-ROOM-001"); mockMvc.perform(signedPost(body, "nonce-v4-unknown-room-type-001")) @@ -1335,6 +1335,34 @@ class SuperAgentTaskResultControllerTest { .contains("房型代码不在第一版目录中"); } + @Test + void shouldMarkV4BusinessCardReviewRequiredWhenLegacyCatalogCodesSubmitted() throws Exception { + SourceMessageCaptureResult source = captureSourceMessage("mail-v4-legacy-catalog-code-001"); + String body = v4BusinessRootBody("mail-v4-legacy-catalog-code-001") + .replace("\"rate_code\": \"GRPA2-850UP\"", "\"rate_code\": \"BAR\"") + .replace("\"room_type_code\": \"RM2\"", "\"room_type_code\": \"TWN\"") + .replace("GRP-V4-001", "GRP-V4-LEGACY-CATALOG-001"); + + mockMvc.perform(signedPost(body, "nonce-v4-legacy-catalog-code-001")) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.accepted_count").value(2)); + + String roomValidationErrors = jdbcTemplate.queryForObject(""" + SELECT validation_errors_json + FROM workflow_reservation_v4_task_card + WHERE source_message_id = ? + AND card_type = 'ROOM_INFORMATION' + AND card_status = 'REVIEW_REQUIRED' + AND review_status = 'PENDING' + LIMIT 1 + """, String.class, source.inboxId()); + assertThat(roomValidationErrors) + .contains("business_fields.rate_code") + .contains("Rate Code 不在第一版目录中") + .contains("business_fields.room_items.0.room_type_code") + .contains("房型代码不在第一版目录中"); + } + @Test void shouldMarkV4UpdateBookingReviewRequiredWhenNestedRoomTypeCodeUnknown() throws Exception { SourceMessageCaptureResult source = captureSourceMessage("mail-v4-update-unknown-nested-room-001"); @@ -1419,7 +1447,7 @@ class SuperAgentTaskResultControllerTest { .replace("GRP-V4-CTR-001", "GRP-V4-TRACE-INVALID-EXTRA-BED-COUNT-001") .replace("\"item_type\": \"GENERAL\"", "\"item_type\": \"EXTRA_BED\"") .replace("\"text\": \"Guest requests quiet rooms.\"", "\"text\": \"Please add one extra bed.\"") - .replace("\"department_code\": \"FO\"", "\"target_room_type_code\": \"TWN\",\n" + .replace("\"department_code\": \"FO\"", "\"target_room_type_code\": \"RM2\",\n" + " \"extra_bed_room_count\": 0,\n" + " \"department_code\": \"FO\""); @@ -3993,11 +4021,11 @@ class SuperAgentTaskResultControllerTest { }, "arrival_date": "2026-07-26", "departure_date": "2026-07-29", - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "booking_scenario": "STANDARD", "room_items": [ { - "room_type_code": "TWN", + "room_type_code": "RM2", "room_count": 2 } ], @@ -4067,11 +4095,11 @@ class SuperAgentTaskResultControllerTest { }, "arrival_date": "2026-08-01", "departure_date": "2026-08-03", - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "booking_scenario": "STANDARD", "room_items": [ { - "room_type_code": "TWN", + "room_type_code": "RM2", "room_count": 2 } ], @@ -4087,12 +4115,12 @@ class SuperAgentTaskResultControllerTest { }, "arrival_date": "2026-08-05", "departure_date": "2026-08-06", - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "booking_scenario": "STANDARD", "guest_name": "TEST GUEST", "room_items": [ { - "room_type_code": "KING", + "room_type_code": "RM3", "room_count": 1 } ], @@ -4137,12 +4165,12 @@ class SuperAgentTaskResultControllerTest { }, "arrival_date": "2026-08-05", "departure_date": "2026-08-06", - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "booking_scenario": "STANDARD", "guest_name": "BOOKING CODE GUEST", "room_items": [ { - "room_type_code": "KING", + "room_type_code": "RM3", "room_count": 1 } ], @@ -4248,11 +4276,11 @@ class SuperAgentTaskResultControllerTest { }, "arrival_date": "2026-08-01", "departure_date": "2026-08-03", - "rate_code": "BAR", + "rate_code": "GRPA2-850UP", "booking_scenario": "STANDARD", "room_items": [ { - "room_type_code": "TWN", + "room_type_code": "RM2", "room_count": 2 } ], @@ -4538,7 +4566,7 @@ class SuperAgentTaskResultControllerTest { }, "after": { "arrival_date": "2026-08-01", - "rate_code": "BAR" + "rate_code": "GRPA2-850UP" }, "manual_review": null }