修复V4房型信息复核与自动DEF展示
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
| 最近更新 | 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 后端联动、Account + booking type 过滤 Rate Code 文档口径、Payment 附件预览文档口径、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` 自行推导;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` 这类安全语义,不暴露内部列名。V4 任务详情 smoke 修复已完成:页面顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面底部,只通过 SourceMessage conversation 接口定位当前触发邮件并默认折叠正文;Basic Information 的展示和确认 payload 不再返回 Agent `target_order`。Rooming List 卡确认时已实现 Group 自动置 `DEF`:如同订单存在可更新的已确认 Room Information 快照,后端会覆盖其 `group_booking_status=DEF` 并写 `V4_ROOMING_LIST_AUTO_DEF` 审计;刷新任务详情时 `display_payload`、`confirmed_payload` 和订单总览均以 DEF 后的确认快照为准;如没有可更新投影,Rooming List 确认仍成功,只写安全审计提示,不临时创建不完整 Room Information。已确认 Rate Code 下一阶段按 Reservation Account + `booking_type`(GROUP / FIT)过滤和校验,不按全酒店 Rate Code 全量展示;已确认 Payment 卡展示付款凭证附件时,`attachment_ids[]` 第一版只读,前端只展示并确认卡片,不增删或替换附件集合,图片在卡片内显示缩略图并点击大图预览,非图片统一文件列表 + 下载,附件外链仍走 SourceMessage 原文权限链路;已确认 Rooming List 任务卡第一版只做事项确认,不做名单解析、附件预览、Excel 生成或 PMS 导入,用户点击“确认卡片”表示已人工处理该 Rooming List 事项;已确认 `REVIEW_REQUIRED` 仍是原业务卡复核态,页面按钮统一叫“确认卡片”,复核态允许编辑当前卡 `fields[]` 白名单内业务字段,问题字段红字提示。后续可继续做测试机 V4 smoke 复测、Rooming List 前端轻量卡展示、Payment 附件预览、Account 范围 Rate Code lookup、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 |
|
||||
| 当前重点 | 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` 自行推导;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` 这类安全语义,不暴露内部列名。V4 任务详情 smoke 修复已完成:页面顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面底部,只通过 SourceMessage conversation 接口定位当前触发邮件并默认折叠正文;Basic Information 的展示和确认 payload 不再返回 Agent `target_order`。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。已确认 Rate Code 下一阶段按 Reservation Account + `booking_type`(GROUP / FIT)过滤和校验,不按全酒店 Rate Code 全量展示;已确认 Payment 卡展示付款凭证附件时,`attachment_ids[]` 第一版只读,前端只展示并确认卡片,不增删或替换附件集合,图片在卡片内显示缩略图并点击大图预览,非图片统一文件列表 + 下载,附件外链仍走 SourceMessage 原文权限链路;已确认 Rooming List 任务卡第一版只做事项确认,不做名单解析、附件预览、Excel 生成或 PMS 导入,用户点击“确认卡片”表示已人工处理该 Rooming List 事项;已确认 `REVIEW_REQUIRED` 仍是原业务卡复核态,页面按钮统一叫“确认卡片”,复核态允许编辑当前卡 `fields[]` 白名单内业务字段,问题字段红字提示。后续可继续做测试机 V4 smoke 复测、Rooming List 前端轻量卡展示、Payment 附件预览、Account 范围 Rate Code lookup、真实 PMS / OPERA / OHIP 同步或 SuperAgent 目录供给方案。 |
|
||||
|
||||
## 1. 当前 Checkpoint
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
| `GET /api/reservation/tasks` | 查询任务列表 / 工作台 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`;未传 `order_id` 时按来源消息接收时间倒序,传 `order_id` 时按同订单队列顺序正序;用 `can_process` 和 `readonly_reason_code` 控制入口按钮;列表不返回 AI 原始 payload、邮件正文或附件 URL;已返回来源邮件会话摘要字段,并支持 `order_status` 按任务所属订单状态筛选;旧 S000/S999 和 V3 S10/S99 以 `task_type=SOURCE_MESSAGE_ONLY` 只读任务返回,列表已透出 `result_type`、`ai_task_type`、`route_code`、`system_process_category`。V4 S10/S99 不再进入该旧任务表,应从 V4 工作台来源通知接口展示。 |
|
||||
| `GET /api/reservation/workbench-items` | 查询 V4 工作台统一列表 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`;返回 V4 业务订单任务和 S10/S99 来源通知混排摘要;支持 `hotel_id`、`item_type`、`keyword`、`page_num`、`page_size`;默认按 `source_received_at` 倒序,同一来源时间下按 `updated_at`、`created_at`、数字 `target_id` 倒序;列表不返回邮件正文、附件 URL、`ai_payload_json` 或来源通知原始 payload。 |
|
||||
| `GET /api/reservation/order-tasks` | 查询 V4 业务订单任务列表 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`;只返回 V4 业务订单任务,不包含 S10/S99 来源通知;支持 `hotel_id`、`order_id`、`order_task_status`、`card_status`、`keyword`、`page_num`、`page_size`;`order_task_status` 非 `OPEN` / `COMPLETED` 返回 400,`card_status` 非 V4 卡状态返回 400;`card_status` 只筛业务 / 可处理卡,固定来源邮件展示卡不参与筛选。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | 查询 V4 订单任务详情 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,后端按订单任务实际酒店校验访问权;返回 `order_task`、`source_message_summary`、`source_message_card`、`basic_information_card`、`business_cards[]`、`card_counts`、`adapter_contract_errors[]` 和 `availability`;来源摘要按酒店过滤,邮件正文和附件仍走 SourceMessage 会话接口。V4 任务详情页展示顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面最下方,正文限定为当前触发该 order task 的 SourceMessage 正文,前端用 `source_message_summary.source_message_id` 调用 `GET /api/source-messages/{sourceMessageId}/conversation` 后定位当前邮件。Payment 卡下一阶段可返回 `payment_attachments[]` 安全摘要用于展示凭证附件,但本接口不得返回附件 URL;图片缩略图 / 大图和非图片下载 URL 仍通过 SourceMessage 会话权限链路取得。CP8 起每张 V4 任务卡返回 `fields[]`,前端应以该字段白名单渲染可编辑控件。Room Information 卡已新增 `display_payload.room_information` 稳定展示模型,前端优先读取 `current_values` / `proposed_values` / `final_values` / `change_summary[]`,不要再从 Agent raw payload、`business_fields` 或 `target_order` 自行推导业务展示。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}/audits` | 查询 V4 订单任务审计流水 | 必须带 Bearer token,需要 `RESERVATION_AUDIT_READ`,后端按订单任务实际酒店校验访问权;返回 `order_task_id` 和 `items[]`。`items[]` 用于展示 V4 卡片确认、复核解阻和订单归属确认轨迹,只包含脱敏后的审计摘要,不包含邮件正文、HTML、附件 URL、AI 原始 payload、token 或 secret。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | 查询 V4 订单任务详情 | 必须带 Bearer token,需要 `RESERVATION_TASK_READ`,后端按订单任务实际酒店校验访问权;返回 `order_task`、`source_message_summary`、`source_message_card`、`basic_information_card`、`business_cards[]`、`card_counts`、`adapter_contract_errors[]` 和 `availability`;来源摘要按酒店过滤,邮件正文和附件仍走 SourceMessage 会话接口。V4 任务详情页展示顺序固定为 Basic Information、业务卡、SourceMessage Display;来源邮件卡位于页面最下方,正文限定为当前触发该 order task 的 SourceMessage 正文,前端用 `source_message_summary.source_message_id` 调用 `GET /api/source-messages/{sourceMessageId}/conversation` 后定位当前邮件。Payment 卡下一阶段可返回 `payment_attachments[]` 安全摘要用于展示凭证附件,但本接口不得返回附件 URL;图片缩略图 / 大图和非图片下载 URL 仍通过 SourceMessage 会话权限链路取得。CP8 起每张 V4 任务卡返回 `fields[]`,前端应以该字段白名单渲染可编辑控件;`write_target` 只返回 `confirmed_payload`、`review_resolution.field_overrides`、`none` 等前端安全语义,不暴露内部列名。Room Information 卡已新增 `display_payload.room_information` 稳定展示模型,前端优先读取 `current_values` / `proposed_values` / `final_values` / `change_summary[]`,不要再从 Agent raw payload、`business_fields` 或 `target_order` 自行推导业务展示;Basic Information 的 `display_payload` / `confirmed_payload` 不返回 Agent `target_order`。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}/audits` | 查询 V4 订单任务审计流水 | 必须带 Bearer token,需要 `RESERVATION_AUDIT_READ`,后端按订单任务实际酒店校验访问权;返回 `order_task_id` 和 `items[]`。`items[]` 用于展示 V4 卡片确认、复核解阻、订单归属确认轨迹和 `V4_ROOMING_LIST_AUTO_DEF` 自动 DEF 摘要,只包含脱敏后的审计摘要,不包含邮件正文、HTML、附件 URL、AI 原始 payload、token 或 secret。 |
|
||||
| `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 以后端确认派生结果为准。 |
|
||||
@@ -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` 并写审计;没有可更新投影时确认仍成功且不创建不完整 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`、写 `confirmed_payload_json/confirmed_at/confirmed_by` 并锁定,重复确认返回错误;成功返回刷新后的订单任务详情。 |
|
||||
| `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}/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 字符串当附件或正文直渲。 |
|
||||
@@ -309,7 +309,7 @@ Content-Type: application/json
|
||||
- `extracted_fields.room_items.0.room_type_raw` 是房型原文证据,第一版返回 `control_type=readonly`、`edit_scope=never`、`raw_readonly=true`;用户应确认或修改 `pms_room_type_code`,不要覆盖 raw 原文。
|
||||
- `extracted_fields.room_items.0.room_quantity` 返回 `control_type=number`;`extracted_fields.room_items.0.pms_room_type_code` 返回 `control_type=select`、`options_source=active_pms_room_type_catalog`。
|
||||
- type-known manual review 的 `manual_review.missing_fields[]` 应按 JSON Pointer 匹配 `fields[].field_pointer`,并复用对应字段控件提交 `field_overrides[]`;匹配不到的 pointer 不要临时生成任意输入框。
|
||||
- 缺失字段会返回 `edit_scope=manual_review_only` 和 `write_target=review_resolution.field_overrides`;同卡复核中其他可编辑业务字段可能返回 `normal_and_manual_review`,前端第一版仍优先只渲染 `missing_fields[]` 指向的字段。
|
||||
- 缺失字段会返回 `edit_scope=manual_review_only` 和 `write_target=review_resolution.field_overrides`;同卡复核中其他可编辑业务字段也可能按当前卡白名单返回 `editable=true`,前端不要只渲染 `missing_fields[]` 指向的字段,应该以 `fields[]` 中真实 `editable` 状态为准。
|
||||
- `field_overrides[]` 新页面优先提交 `field_pointer`,可同时提交 `fields[]` 中的主 `field_path`;不要提交旧扁平 key 作为新逻辑首选。
|
||||
- `options_source=active_pms_room_type_catalog`、`rate_code_catalog`、`system_case_lookup` 第一版仅代表选项来源,真实目录 / lookup 未接入前,前端不得硬编码 PMS 房型、Rate Code 或系统对象全集。
|
||||
- 后端可能返回 `control_hint=catalog_backend_pending`、`lookup_backend_pending`、`structured_table_editor_pending`,用于提示前端目录、lookup 或表格编辑后端能力仍未接入。
|
||||
@@ -499,7 +499,7 @@ RESERVATION_ROOMING_LIST_GENERATE
|
||||
- `/reservation/order-tasks/{orderTaskId}` 应使用 `GET /api/reservation/order-tasks/{orderTaskId}` 展示 `basic_information_card`、`business_cards[]`、`source_message_card`、`card_counts`、`order_task` 摘要和 `adapter_contract_errors[]` 只读诊断;页面顺序固定为 Basic Information、业务卡、SourceMessage Display。
|
||||
- `/reservation/source-notifications/{notificationId}` 应使用 `GET /api/reservation/source-notifications/{notificationId}` 展示 S10/S99 来源通知详情,并通过 `POST /api/reservation/source-notifications/{notificationId}/ack` 确认已读 / 已处理。
|
||||
- V4 审计时间线应分别调用 `GET /api/reservation/order-tasks/{orderTaskId}/audits` 和 `GET /api/reservation/source-notifications/{notificationId}/audits`;按钮权限使用 `/api/auth/me.permissions[]` 中的 `RESERVATION_AUDIT_READ`。
|
||||
- V4 卡片确认应使用 `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm`,请求带 `version`;前端只从当前卡 `fields[]` 中挑选 `editable=true`、`raw_readonly!=true`、`write_target` 指向确认 payload 的字段构造 `confirmed_payload`。
|
||||
- V4 卡片确认应使用 `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/confirm`,请求带 `version`;前端只从当前卡 `fields[]` 中挑选 `editable=true`、`raw_readonly!=true`、`write_target=confirmed_payload` 的字段构造 `confirmed_payload`。
|
||||
- V4 复核解阻应使用 `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution`,请求带 `version`;`field_overrides[]` 只使用当前卡 `fields[].field_pointer`,订单任务归属未解决时允许用户填写 `confirmed_order_id`。
|
||||
- 所有按钮应按后端 `availability.confirmable`、`availability.reviewable`、`availability.ackable` 和前端权限共同控制;不可操作原因优先展示 `readonly_reason_message`,否则按 `readonly_reason_code` 做友好映射。
|
||||
- `fields[].validation_errors` 应展示在对应字段旁边;接口返回 `V4_FIELD_VALIDATION_FAILED` 且 `details[]` 带嵌套路径时,前端应尝试定位到对应 field,定位不到则在当前卡片动作错误区展示。
|
||||
@@ -526,9 +526,9 @@ RESERVATION_ROOMING_LIST_GENERATE
|
||||
- M002 V4 入站解析与数据模型基线已完成第一版:后端可接收 `source_message + order_contexts[] + message_events[]`,识别 `NEW_BOOKING`、`UPDATE_BOOKING`、`CANCEL_BOOKING`、`TRACE_RESERVATION_NOTES`、`ROOMING_LIST`、`PAYMENT`,并保存 V4 原始 payload、`route_code`、系统处理分类和 `field_contract_version=20260718-v4`。前端暂不需要直接调用 V4 回调接口。
|
||||
- M002 V4 CP5 已完成查询接口:普通 V4 业务包可通过 `/api/reservation/workbench-items`、`/api/reservation/order-tasks`、`/api/reservation/order-tasks/{orderTaskId}` 查看;V4 S10/S99 来源通知可通过 `/api/reservation/source-notifications/{notificationId}` 查看。M002 V4 CP6 已开放普通卡片确认和 S10/S99 ack 写接口;M002 V4 CP7 已开放 `POST /api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution` 复核解阻接口;M002 V4 CP8 已开放 V4 卡片 `fields[]` 白名单和目录校验;M002 V4 CP11 已把固定种子迁移到数据库目录,并开放 Account / Room Type / Rate Code lookup API;目录管理后台 CP1 已开放 Account / Room Type / Rate Code 后端列表、新增、启用 / 停用接口;V4 业务审计查询已补齐订单任务审计和来源通知 ack 审计两个只读接口。
|
||||
- V4 任务卡的 `display_payload_json` 只保留后端白名单展示字段;`ai_payload_json` 才包含完整 SuperAgent 原始 event。后续 V4 查询接口不得把 `ai_payload_json`、附件 URL 或 raw evidence 直接给普通页面渲染;前端对来源消息卡附件字段仍做 URL-like 文本兜底脱敏。
|
||||
- Room Information 卡已由后端返回业务展示模型,前端已按该模型完成第一版业务化展示,不再自行从 Agent raw payload、`business_fields` 或 `target_order` 计算。读取路径是业务卡 `display_payload.room_information`:`NEW_BOOKING` 展示最终值,Group 的最终订单投影字段 `group_block_name` 可编辑,默认来自 `target_order.locator_value` 且 `locator_type=GROUP_CODE`;Fit 的最终订单投影字段 `fit_name` 可编辑,默认来自 `guest_name ?? target_order.locator_value`;Agent 原始 `target_order.locator_value` 始终只读,用户编辑只影响本系统最终订单投影和确认快照。`UPDATE_BOOKING` 顶部展示本地当前值到 Agent 修改后值的 `change_summary[]`,日期变化时连带展示 Nights 差异,字段区展示合并后的最终值;`CANCEL_BOOKING` 从本地订单投影只读展示 current / final 模型,不显示编辑控件。Nights 由后端按酒店本地日期派生,前端只展示不计算;Adult 不显示。
|
||||
- Room Information 卡已由后端返回业务展示模型,前端已按该模型完成第一版业务化展示,不再自行从 Agent raw payload、`business_fields` 或 `target_order` 计算。读取路径是业务卡 `display_payload.room_information`:`NEW_BOOKING` 展示最终值,Group 的最终订单投影字段 `group_block_name` 可编辑,默认来自 Agent `target_order.locator_value` 且 `locator_type=GROUP_CODE`;Fit 的最终订单投影字段 `fit_name` 可编辑,默认来自 `guest_name ?? target_order.locator_value`;Agent 原始 `target_order` 不在普通 `display_payload` / `confirmed_payload` 中暴露,用户编辑只影响本系统最终订单投影和确认快照。`UPDATE_BOOKING` 顶部展示本地当前值到 Agent 修改后值的 `change_summary[]`,日期变化时连带展示 Nights 差异,字段区展示合并后的最终值;`CANCEL_BOOKING` 从本地订单投影只读展示 current / final 模型,不显示编辑控件。Nights 由后端按酒店本地日期派生,前端只展示不计算;Adult 不显示。
|
||||
- Room Information 卡 Breakfast 前端显示为“含早”勾选框:Group 固定勾选且只读;Fit 由后端按最终 Rate Code 中 `RB` / `RO` 派生,无法派生时作为必填勾选项。Group Booking Status 仅 Group 显示,稳定 code 为 `TEN` / `DEF` / `INQ`,展示文案为 `TEN-Tentative`、`DEF-Definite`、`INQ-Inquiry`;New Group 默认 `TEN`,`NEW_BOOKING` / `UPDATE_BOOKING` 确认前可改选,`CANCEL_BOOKING` 只读。
|
||||
- Rooming List 卡确认存在跨卡联动:同订单为 Group 时,确认 `ROOMING_LIST` 后后端已把 Group Booking Status 自动置为 `DEF`,即使此前为 `TEN` 或 `INQ`;Fit 不显示也不变更该状态。该自动变更由后端写 `V4_ROOMING_LIST_AUTO_DEF` 审计,前端只展示刷新后的状态;如果没有可更新 Room Information 投影,确认仍成功,后端只写安全审计提示。
|
||||
- Rooming List 卡确认存在跨卡联动:同订单为 Group 时,确认 `ROOMING_LIST` 后后端已把 Group Booking Status 自动置为 `DEF`,即使此前为 `TEN` 或 `INQ`;Fit 不显示也不变更该状态。该自动变更由后端写 `V4_ROOMING_LIST_AUTO_DEF` 审计,并在后续任务详情刷新时让 Room Information 的 `display_payload.room_information.final_values` 和 `confirmed_payload.room_information.final_values` 保持 DEF 口径一致;当前订单详情 `order_overview` 不返回 Group Booking Status 字段,仍只展示既有确认快照字段;如果没有可更新 Room Information 投影,确认仍成功,后端只写安全审计提示。
|
||||
- Rooming List 卡第一版是轻量事项确认卡:前端展示卡片标题、状态、目标订单信息和“确认卡片”按钮即可;不要做名单 rows、附件预览、Excel 生成或 PMS 导入入口。确认仅表示该 Rooming List 事项已人工处理。
|
||||
- Payment 卡下一阶段建议由后端在 `display_payload_json.payment_attachments[]` 返回安全摘要,字段只包含附件 ID、文件名、类型、大小、是否图片、是否可预览 / 下载等,不包含外链。第一版 `attachment_ids[]` 是 Agent 返回的只读业务事实,前端只展示并确认卡片,不允许用户增删、替换或重新选择附件集合,也不把 `attachment_ids[]`、`externalUrl` 或完整附件对象提交回确认接口。
|
||||
- V4 复核态卡片仍是原业务卡,不新建单独复核任务卡;页面状态显示“需要复核”,问题字段用 `fields[].validation_errors` 红字提示,主按钮文案统一为“确认卡片”。前端内部必须根据 `card_status=REVIEW_REQUIRED` 调用 `review-resolution`,不要调用普通 `confirm`。
|
||||
@@ -539,10 +539,10 @@ RESERVATION_ROOMING_LIST_GENERATE
|
||||
- M002 V4 CP3 已新增 V4 订单任务、任务卡、S10/S99 来源通知三张表和 Repository 基线;M002 V4 CP4 已把正式 V4 回调写入这些表;M002 V4 CP5 已开放查询;M002 V4 CP6 已开放普通卡片确认和 S10/S99 ack;M002 V4 CP7 已开放复核解阻与复核场景订单归属确认;M002 V4 CP8 已开放目录校验和 V4 任务卡 `fields[]` 字段白名单;M002 V4 CP11 已新增目录表、数据库种子和 lookup API;目录管理后台 CP1 已新增 `RESERVATION_CATALOG_MANAGE` 后端接口;V4 审计查询已开放 `RESERVATION_AUDIT_READ` 下的订单任务审计和来源通知审计。
|
||||
- V4 订单任务和卡片 `availability` 已新增 `reviewable`。当前语义:`REVIEW_REQUIRED` 卡如果未被 Basic Information 或前置订单任务阻塞,会返回 `read_only=false`、`editable=true`、`confirmable=false`、`reviewable=true`、`readonly_reason_code=PROCESSABLE`;前端应调用 `review-resolution`,不要调用普通 `confirm`。
|
||||
- CP11 起 V4 入站阶段按当前酒店数据库目录做校验:Account 缺失或不存在时 Basic Information 卡直接 `REVIEW_REQUIRED`;业务卡已有 `room_items[].room_type_code` 或 `rate_code` 但不在当前酒店目录时,业务卡也会直接 `REVIEW_REQUIRED`,错误会回显在 `fields[].validation_errors`。
|
||||
- CP8 / Room Information 展示模型后,确认接口按 `fields[]` 白名单收口:前端可以只提交用户修改过的可编辑字段,不建议整包回传 `display_payload`。后端会从当前卡展示快照生成确认快照,并只合并可写叶子字段;来源邮件、路由、`target_order`、`order_ref`、`manual_review`、校验诊断字段以及前端额外注入字段不会写入 `confirmed_payload_json`。
|
||||
- CP8 / Room Information 展示模型后,确认接口按 `fields[]` 白名单收口:前端可以只提交用户修改过的可编辑字段,不建议整包回传 `display_payload`。后端会从当前卡展示快照生成确认快照,并只合并可写叶子字段;来源邮件、路由、`target_order`、`order_ref`、`manual_review`、校验诊断字段以及前端额外注入字段不会写入内部确认快照。
|
||||
- 业务卡目录校验会递归检查稳定模型或历史兼容结构。例如 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`;前端不要自行补未返回字段。
|
||||
- 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;前端不要自行补未返回字段。
|
||||
- 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[]`。
|
||||
- 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 多卡最终接口。
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
| `GET /api/reservation/tasks` | 已完成第一版,已补来源邮件会话字段和所属订单状态筛选 | 可以 | `order_status` 按任务所属订单状态过滤;不传时保持当前全部任务列表行为。 |
|
||||
| `GET /api/reservation/orders/{orderId}` | 已完成第一版,已补旧 `tasks[]` 来源邮件会话字段、V4 总览和 V4 `v4_order_tasks[]` 时间线,前端订单详情总览页已接入 | 可以 | 暂无;`include_tasks=false` 时旧 `tasks[]`、V4 `v4_order_tasks[]` 和 `related_source_messages[]` 都返回空数组,`order_overview` 为空快照,`next_v4_action.action_type=NONE`。 |
|
||||
| `GET /api/reservation/tasks/{taskId}` | 已完成第一版,已补任务顶层来源邮件字段和 `fields[]` 3.0 元数据 | 可以 | 当前 Controller 不接收 `hotel_id`;如后续多酒店隔离需要前端显式传酒店上下文,请后端补可选入参或确认按 taskId 全局唯一即可。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | 已完成第一版;Room Information 后端展示模型已补齐且前端业务化展示已接入;Rooming List 确认自动 DEF 后端联动已完成;复核态字段白名单第一版已随 `fields[]` 返回;Payment 附件安全摘要仍待补齐 | 可以;Room Information 前端业务化展示已完成,Rooming List 轻量确认可继续进入前端业务化展示,Payment 预览仍需后端补摘要后再完整联动 | V4 任务详情页展示顺序为 Basic Information、业务卡、SourceMessage Display;Trace 卡 `department_code` 第一版固定为 `FO` / `HSK` / `FO+HSK` 三个下拉值,不调用 Department lookup,不开放自由输入。Room Information 已由后端返回 `display_payload.room_information`:New 展示最终值,Update 展示 `current_values`、`proposed_values`、`final_values` 和 `change_summary[]`,Cancel 展示本地订单投影只读;Nights 后端按酒店本地日期派生,Breakfast 前端为含早勾选框,Group Booking Status 显示 `TEN-Tentative` / `DEF-Definite` / `INQ-Inquiry`;New Booking 最终订单投影字段 `group_block_name` / `fit_name` 可编辑,Group 默认来自 `target_order.locator_value` 且 `locator_type=GROUP_CODE`,Fit 默认来自 `guest_name ?? target_order.locator_value`,但 Agent 原始 `target_order.locator_value` 只读且不被用户编辑回写。`fields[]` 中 Room Information 字段统一使用 `/room_information/final_values/...`,确认 payload 和复核 `field_overrides[]` 均优先使用这些 pointer。`REVIEW_REQUIRED` 仍是原业务卡复核态,问题字段红字提示,按钮统一显示“确认卡片”,前端内部调用 `review-resolution`。Rooming List 卡第一版只做事项确认,前端展示标题、状态、目标订单信息和“确认卡片”按钮,不做名单 rows、附件预览、Excel 生成或 PMS 导入;确认 `ROOMING_LIST` 后,如同订单为 Group,后端会自动把已确认 Room Information 快照中的 Group Booking Status 置为 `DEF`,前端刷新后展示即可,不需要自行提交或计算该状态。本接口仍不直接返回邮件正文或附件 URL。来源邮件卡正文限定为当前触发该 V4 order task 的那封 SourceMessage 正文,前端用 `source_message_summary.source_message_id` 调用 `GET /api/source-messages/{sourceMessageId}/conversation` 后定位当前邮件,默认长度折叠并可展开;缺少 `SOURCE_MESSAGE_ORIGINAL_READ` 或会话接口失败时降级展示安全摘要。Payment 卡下一阶段建议返回 `payment_attachments[]` 安全摘要,供前端展示图片缩略图 / 非图片文件列表;`attachment_ids[]` 第一版只读,不支持前端增删、替换或重新选择附件集合;实际大图预览和下载 URL 仍走 SourceMessage conversation。 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | 已完成第一版;Room Information 后端展示模型已补齐且前端业务化展示已接入;Rooming List 确认自动 DEF 后端联动已完成;复核态字段白名单第一版已随 `fields[]` 返回;Payment 附件安全摘要仍待补齐 | 可以;Room Information 前端业务化展示已完成,Rooming List 轻量确认可继续进入前端业务化展示,Payment 预览仍需后端补摘要后再完整联动 | V4 任务详情页展示顺序为 Basic Information、业务卡、SourceMessage Display;Trace 卡 `department_code` 第一版固定为 `FO` / `HSK` / `FO+HSK` 三个下拉值,不调用 Department lookup,不开放自由输入。Room Information 已由后端返回 `display_payload.room_information`:New 展示最终值,Update 展示 `current_values`、`proposed_values`、`final_values` 和 `change_summary[]`,Cancel 展示本地订单投影只读;Nights 后端按酒店本地日期派生,Breakfast 前端为含早勾选框,Group Booking Status 显示 `TEN-Tentative` / `DEF-Definite` / `INQ-Inquiry`;New Booking 最终订单投影字段 `group_block_name` / `fit_name` 可编辑,默认值可来自 Agent `target_order`,但 Agent 原始 `target_order` 不在普通 `display_payload` / `confirmed_payload` 中暴露,也不被用户编辑回写。`fields[]` 中 Room Information 字段统一使用 `/room_information/final_values/...`,确认 payload 和复核 `field_overrides[]` 均优先使用这些 pointer;`write_target=confirmed_payload` 是前端请求体语义,不是后端表字段名。`REVIEW_REQUIRED` 仍是原业务卡复核态,问题字段红字提示,按钮统一显示“确认卡片”,前端内部调用 `review-resolution`,并以 `fields[].editable` 渲染当前卡白名单字段,不只渲染 missing/error 字段。Rooming List 卡第一版只做事项确认,前端展示标题、状态、目标订单信息和“确认卡片”按钮,不做名单 rows、附件预览、Excel 生成或 PMS 导入;确认 `ROOMING_LIST` 后,如同订单为 Group,后端会自动把已确认 Room Information 快照中的 Group Booking Status 置为 `DEF`,后续刷新任务详情的 `display_payload` 和 `confirmed_payload` 都会显示 DEF,不需要前端自行提交或计算该状态,并可通过订单任务审计看到 `V4_ROOMING_LIST_AUTO_DEF`;当前订单详情 `order_overview` 不返回 Group Booking Status 字段。本接口仍不直接返回邮件正文或附件 URL。来源邮件卡正文限定为当前触发该 V4 order task 的那封 SourceMessage 正文,前端用 `source_message_summary.source_message_id` 调用 `GET /api/source-messages/{sourceMessageId}/conversation` 后定位当前邮件,默认长度折叠并可展开;缺少 `SOURCE_MESSAGE_ORIGINAL_READ` 或会话接口失败时降级展示安全摘要。Payment 卡下一阶段建议返回 `payment_attachments[]` 安全摘要,供前端展示图片缩略图 / 非图片文件列表;`attachment_ids[]` 第一版只读,不支持前端增删、替换或重新选择附件集合;实际大图预览和下载 URL 仍走 SourceMessage conversation。 |
|
||||
| `PUT /api/reservation/tasks/{taskId}/draft` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 |
|
||||
| `POST /api/reservation/tasks/{taskId}/confirm` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如写操作需要酒店上下文幂等 / 权限校验,请后端补可选入参或请求体字段。 |
|
||||
| `GET /api/reservation/tasks/{taskId}/audits` | 已完成 | 可以 | 当前 Controller 不接收 `hotel_id`;如审计查询需要酒店上下文隔离,请后端补可选入参。 |
|
||||
@@ -1201,3 +1201,4 @@ POST /api/reservation/tasks/{taskId}/order-binding
|
||||
- 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 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`;Rooming List 自动 DEF 后刷新任务详情的 Room Information `display_payload` / `confirmed_payload` 应显示 DEF,审计接口返回 `V4_ROOMING_LIST_AUTO_DEF`;当前订单详情 `order_overview` 不返回 Group Booking Status 字段。
|
||||
|
||||
@@ -571,16 +571,16 @@ Room Information 卡展示模型:
|
||||
|
||||
- `ROOM_INFORMATION` 卡只由 `NEW_BOOKING`、`UPDATE_BOOKING`、`CANCEL_BOOKING` 三类 event 触发;`TRACE_RESERVATION_NOTES`、`ROOMING_LIST`、`PAYMENT` 不触发房型信息卡。
|
||||
- SuperAgent 仍只输出字段契约中的业务字段。Nights、Breakfast、Group Booking Status、Block ID、Confirmation Number 和 Adult 不由 SuperAgent 输出;其中 Adult 第一版不在卡内展示。
|
||||
- 后端已在 `GET /api/reservation/order-tasks/{orderTaskId}` 的 Room Information 业务卡 `display_payload.room_information` 中补稳定展示模型,结构为 `event_type`、`booking_type`、`current_values`、`proposed_values`、`final_values`、`change_summary[]`、`group_booking_status_options[]`。前端按该展示模型渲染业务 UI,不再从 Agent raw payload / `target_order` 自行推导;`fields[]` 继续作为确认 / 复核的可编辑字段白名单。
|
||||
- `fields[]` 的 Room Information 主路径统一为 `/room_information/final_values/...`,例如 `/room_information/final_values/arrival_date`、`/room_information/final_values/room_items/0/room_type_code`。确认接口收到该结构时,后端会从展示模型派生 `confirmed_payload_json.room_information.final_values`,并重新计算 `nights`、`breakfast_included` 和 `group_booking_status_label`;只读字段、Agent `target_order`、Adult 和前端注入字段不会写入确认快照。
|
||||
- 后端已在 `GET /api/reservation/order-tasks/{orderTaskId}` 的 Room Information 业务卡 `display_payload.room_information` 中补稳定展示模型,结构为 `event_type`、`booking_type`、`current_values`、`proposed_values`、`final_values`、`change_summary[]`、`group_booking_status_options[]`。前端按该展示模型渲染业务 UI,不再从 Agent raw payload / `target_order` 自行推导;`fields[]` 继续作为确认 / 复核的可编辑字段白名单。`fields[].write_target` 对前端只表达请求体目标,例如 `confirmed_payload` 或 `review_resolution.field_overrides`,不暴露后端内部列名。
|
||||
- `fields[]` 的 Room Information 主路径统一为 `/room_information/final_values/...`,例如 `/room_information/final_values/arrival_date`、`/room_information/final_values/room_items/0/room_type_code`。确认接口收到该结构时,后端会从展示模型派生 `confirmed_payload_json.room_information.final_values`,并重新计算 `nights`、`breakfast_included` 和 `group_booking_status_label`;只读字段、Agent `target_order`、Adult 和前端注入字段不会写入确认快照。`REVIEW_REQUIRED` 状态下,当前卡白名单内业务字段可以返回 `editable=true` 并允许同一 pointer 走 `review-resolution`,不再限定只能修空值、`missing_fields[]` 或目录错误字段。
|
||||
- `NEW_BOOKING`:卡片展示创建后的最终值。Agent 提供 `target_order`、`arrival_date`、`departure_date`、`rate_code`、`booking_scenario`、`room_items[]`,Fit 可提供 `guest_name`;后端派生 `nights`、`breakfast_included` 和 Group Booking Status。
|
||||
- `UPDATE_BOOKING`:后端从本地订单投影读取当前值,用 Agent `after` 合并得到最终值;页面上方展示本次实际变化的 `change_summary[]`,例如 `入住日期:2026-07-12 -> 2026-07-20`。如果日期变化导致 `nights` 变化,`nights` 也必须出现在差异区;字段区展示合并后的最终值。
|
||||
- `CANCEL_BOOKING`:不使用 Agent 输出当前订单快照;后端从本地订单投影读取当前值并只读展示,用户只确认整单取消。Cancel 卡不允许编辑 Group Booking Status、Breakfast、日期、Rate Code 或房型房量。
|
||||
- `nights` 由后端按酒店本地业务日期计算:`departure_date - arrival_date`,不涉及时区和 UTC;日期缺失、非法或离店早于入住时,`nights` 为空。第一版确认校验要求日期必填,后续如需更严格营业日规则另开 checkpoint。
|
||||
- `breakfast_included` 是卡片展示和确认使用的布尔字段。Group 固定含早,前端显示勾选且只读;Fit 按 Rate Code 派生,Rate Code 包含 `RB` 时含早,包含 `RO` 时不含早;如果 Rate Code 无法派生,前端显示必填勾选框,由用户确认是否含早。
|
||||
- Group Booking Status 仅 Group 显示,稳定 code 为 `TEN`、`DEF`、`INQ`,前端显示 `TEN-Tentative`、`DEF-Definite`、`INQ-Inquiry`。New Group 默认 `TEN`;`booking_scenario=STANDARD | PROPOSAL` 仅保留为 Agent 场景参考,不映射 Group Booking Status。`NEW_BOOKING` / `UPDATE_BOOKING` 确认前可手动改选,`CANCEL_BOOKING` 只读。
|
||||
- `ROOMING_LIST` 卡确认时,如果同订单为 Group,后端已把 Group Booking Status 自动置为 `DEF`,即使此前为 `TEN` 或 `INQ`;该自动变更写入 `V4_ROOMING_LIST_AUTO_DEF` 业务审计。Fit 不显示也不变更 Group Booking Status。
|
||||
- `target_order.locator_value` 不作为前端可编辑字段;订单归属错误时通过 V4 复核选择正确订单或创建正确订单投影,不直接改写 Agent 原始 `target_order.locator_value`。但 New Booking 创建 / 确认的最终订单投影字段允许编辑:Group 显示并允许编辑 `group_block_name`,默认值来自 `target_order.locator_value` 且 `locator_type=GROUP_CODE`;Fit 显示并允许编辑 `fit_name`,默认值来自 `guest_name ?? target_order.locator_value`。用户修改这些字段只影响本系统最终订单投影和确认快照,不回写 Agent 原始定位字段。
|
||||
- `ROOMING_LIST` 卡确认时,如果同订单为 Group,后端已把 Group Booking Status 自动置为 `DEF`,即使此前为 `TEN` 或 `INQ`;该自动变更写入 `V4_ROOMING_LIST_AUTO_DEF` 业务审计,并且刷新任务详情时 Room Information 的 `display_payload.room_information.final_values`、`confirmed_payload.room_information.final_values` 都以后端 DEF 后的确认快照为准。当前订单详情 `order_overview` 不返回 Group Booking Status 字段,仍只展示既有确认快照字段。Fit 不显示也不变更 Group Booking Status。
|
||||
- `target_order.locator_value` 不作为前端可编辑字段,也不在普通任务详情的 Basic Information `display_payload` / `confirmed_payload` 中返回;订单归属错误时通过 V4 复核选择正确订单或创建正确订单投影,不直接改写 Agent 原始 `target_order.locator_value`。但 New Booking 创建 / 确认的最终订单投影字段允许编辑:Group 显示并允许编辑 `group_block_name`,默认值来自 Agent `target_order.locator_value` 且 `locator_type=GROUP_CODE`;Fit 显示并允许编辑 `fit_name`,默认值来自 `guest_name ?? target_order.locator_value`。用户修改这些字段只影响本系统最终订单投影和确认快照,不回写 Agent 原始定位字段。
|
||||
- Block ID 和 Confirmation Number 第一版只读;存在本地投影或未来 PMS 结果时展示,否则为空。Block ID 仅 Group 显示,Confirmation Number 仅 Fit 显示。
|
||||
|
||||
V4 任务详情页第一版字段白名单:
|
||||
@@ -601,7 +601,7 @@ Rooming List 卡事项确认规则:
|
||||
- Rooming List 卡第一版只做事项确认,不做名单解析、附件预览、Excel 生成或 PMS 导入。
|
||||
- `ROOMING_LIST` event 不输出 `rows[]`、逐人名单、同住分组、18 列、Excel 或 PMS 导入参数,也不要求单独输出 `attachment_ids[]`。
|
||||
- 页面应展示卡片标题、状态、目标订单信息和“确认卡片”按钮;如需查看来源内容,仍通过本订单任务底部的 `SOURCE_MESSAGE_DISPLAY` 查看当前触发 SourceMessage 正文和附件摘要。
|
||||
- 用户点击“确认卡片”表示已人工处理该 Rooming List 事项;该确认会更新 V4 卡片状态和订单任务派生状态;如果同订单为 Group 且存在可更新的已确认 Room Information 快照,后端同时覆盖该快照里的 `group_booking_status=DEF` 和 `group_booking_status_label=DEF-Definite`,不改变 Agent 原始 payload。没有可更新投影时确认仍成功,只记录安全审计提示,不临时创建不完整 Room Information。该动作不代表 M010 Rooming List Excel 已生成,也不代表 PMS / OPERA / OHIP 已执行。
|
||||
- 用户点击“确认卡片”表示已人工处理该 Rooming List 事项;该确认会更新 V4 卡片状态和订单任务派生状态;如果同订单为 Group 且存在可更新的已确认 Room Information 快照,后端同时覆盖该快照里的 `group_booking_status=DEF` 和 `group_booking_status_label=DEF-Definite`,不改变 Agent 原始 payload,并写入可通过 V4 订单任务审计接口查询的 `V4_ROOMING_LIST_AUTO_DEF` 摘要。没有可更新投影时确认仍成功,只记录安全审计提示,不临时创建不完整 Room Information。该动作不代表 M010 Rooming List Excel 已生成,也不代表 PMS / OPERA / OHIP 已执行。
|
||||
- 独立 Rooming List Excel 生成能力仍属于 M010 `/reservation/rooming-lists/new` 工具页面,第一版不嵌入 V4 Rooming List 卡。
|
||||
|
||||
Payment 卡附件展示规则:
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
| `GET /api/reservation/tasks/{taskId}` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_TASK_READ`;按任务实际所属酒店校验访问权 | 保持登录 + `RESERVATION_TASK_READ` + 任务所属酒店访问权 | 只读查询默认不写业务审计 |
|
||||
| `GET /api/reservation/workbench-items` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;统一返回 V4 业务订单任务和 S10/S99 来源通知摘要 | 只读查询默认不写业务审计;不得返回邮件正文、附件 URL、AI 原始 payload 或来源通知原始 payload;同来源时间下使用 `updated_at` / `created_at` / 数字 ID 稳定排序 |
|
||||
| `GET /api/reservation/order-tasks` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 酒店访问权;只返回 V4 业务订单任务,不返回 S10/S99 来源通知 | 只读查询默认不写业务审计;不得返回 AI 原始 payload;`card_status` 只匹配业务 / 可处理卡,固定来源邮件展示卡不参与筛选 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权;V4 任务详情页展示顺序为 Basic Information、业务卡、SourceMessage Display;Room Information 展示模型只返回当前酒店本地订单投影、Agent 白名单字段和系统派生值;Payment 卡可返回付款凭证附件安全摘要;Payment 第一版 `attachment_ids[]` 只读展示,不支持前端增删或替换附件集合 | 只读查询默认不写业务审计;本接口不得直接返回邮件正文、HTML 或附件 URL,来源邮件卡正文和 Payment 图片预览 / 非图片下载必须通过 `GET /api/source-messages/{id}/conversation` 的 SourceMessage 原文权限链路读取;Room Information 展示模型不得返回 PMS 原始响应、价格明细、AI 原始 payload 或跨酒店订单值;Payment 安全摘要只能包含附件 ID、文件名、类型、大小、是否图片、是否可预览 / 下载等;不得返回 `ai_payload_json`;同批次 `adapter_contract_errors[]` 只返回白名单诊断字段;前端普通业务卡如遇 URL-like 附件字符串必须二次脱敏 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}/audits` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_AUDIT_READ` + V4 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_AUDIT_READ` + 订单任务所属酒店访问权;仅返回卡片确认和复核解阻审计摘要 | 查询审计不再写审计;返回快照必须脱敏,不返回原始邮件正文、HTML、附件 URL、AI 原始 payload、token 或 secret |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}` | `FRONTEND_USER` | 已实现 M002 V4 CP5;强制 Bearer 登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_TASK_READ` + 订单任务所属酒店访问权;V4 任务详情页展示顺序为 Basic Information、业务卡、SourceMessage Display;Basic Information 不返回 Agent `target_order`;Room Information 展示模型只返回当前酒店本地订单投影、Agent 白名单字段和系统派生值;`fields[].write_target` 只返回前端安全语义,不暴露内部列名;Payment 卡可返回付款凭证附件安全摘要;Payment 第一版 `attachment_ids[]` 只读展示,不支持前端增删或替换附件集合 | 只读查询默认不写业务审计;本接口不得直接返回邮件正文、HTML 或附件 URL,来源邮件卡正文和 Payment 图片预览 / 非图片下载必须通过 `GET /api/source-messages/{id}/conversation` 的 SourceMessage 原文权限链路读取;Basic Information 和 Room Information 展示模型不得返回 PMS 原始响应、价格明细、AI 原始 payload、Agent 原始 `target_order` 或跨酒店订单值;Payment 安全摘要只能包含附件 ID、文件名、类型、大小、是否图片、是否可预览 / 下载等;不得返回 `ai_payload_json`;同批次 `adapter_contract_errors[]` 只返回白名单诊断字段;前端普通业务卡如遇 URL-like 附件字符串必须二次脱敏 |
|
||||
| `GET /api/reservation/order-tasks/{orderTaskId}/audits` | `FRONTEND_USER` | 已强制 Bearer 登录 + `RESERVATION_AUDIT_READ` + V4 订单任务所属酒店访问权 | 保持登录 + `RESERVATION_AUDIT_READ` + 订单任务所属酒店访问权;返回卡片确认、复核解阻和 `V4_ROOMING_LIST_AUTO_DEF` 自动 DEF 审计摘要 | 查询审计不再写审计;返回快照必须脱敏,不返回原始邮件正文、HTML、附件 URL、AI 原始 payload、token 或 secret |
|
||||
| `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 或外部同步错误详情 |
|
||||
|
||||
@@ -61,7 +61,8 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
|
||||
|
||||
private static final List<String> V4_ORDER_TASK_AUDIT_ACTIONS = List.of(
|
||||
"V4_CARD_CONFIRM",
|
||||
"V4_CARD_REVIEW_RESOLVE");
|
||||
"V4_CARD_REVIEW_RESOLVE",
|
||||
"V4_ROOMING_LIST_AUTO_DEF");
|
||||
private static final String ACTION_V4_SOURCE_NOTIFICATION_ACK = "V4_SOURCE_NOTIFICATION_ACK";
|
||||
|
||||
private final ReservationAiBatchMapper batchMapper;
|
||||
|
||||
@@ -594,7 +594,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
JsonNode submittedFinalValues = submittedPayload.path("room_information").path("final_values");
|
||||
JsonNode submittedRoot = submittedFinalValues.isObject() ? submittedFinalValues : submittedPayload;
|
||||
String bookingType = textAt(payload.path("room_information"), "booking_type");
|
||||
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, submittedRoot, List.of());
|
||||
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, finalValues, submittedRoot, List.of());
|
||||
normalizeRoomInformationDerivedFields(bookingType, finalValues);
|
||||
payload.withObject("/room_information").set("change_summary",
|
||||
changeSummary(payload.path("room_information").path("current_values"), finalValues));
|
||||
@@ -603,6 +603,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
private void overlayEditableRoomInformationLeaves(
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
String bookingType,
|
||||
JsonNode finalValues,
|
||||
ObjectNode target,
|
||||
JsonNode submitted,
|
||||
List<String> path) {
|
||||
@@ -613,11 +614,11 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
while (fields.hasNext()) {
|
||||
Map.Entry<String, JsonNode> field = fields.next();
|
||||
List<String> childPath = appendPath(path, field.getKey());
|
||||
if (!isRoomInformationPathWritable(card, bookingType, childPath)) {
|
||||
if (!isRoomInformationPathWritable(card, bookingType, childPath, finalValues)) {
|
||||
continue;
|
||||
}
|
||||
JsonNode submittedValue = submitted.get(field.getKey());
|
||||
overlayEditableRoomInformationValue(card, bookingType, target, field.getKey(), field.getValue(),
|
||||
overlayEditableRoomInformationValue(card, bookingType, finalValues, target, field.getKey(), field.getValue(),
|
||||
submittedValue, childPath);
|
||||
}
|
||||
}
|
||||
@@ -625,6 +626,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
private void overlayEditableRoomInformationArray(
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
String bookingType,
|
||||
JsonNode finalValues,
|
||||
ArrayNode target,
|
||||
JsonNode submitted,
|
||||
List<String> path) {
|
||||
@@ -634,7 +636,7 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
int size = Math.min(target.size(), submitted.size());
|
||||
for (int index = 0; index < size; index++) {
|
||||
List<String> childPath = appendPath(path, String.valueOf(index));
|
||||
overlayEditableRoomInformationValue(card, bookingType, target, index, target.get(index), submitted.get(index),
|
||||
overlayEditableRoomInformationValue(card, bookingType, finalValues, target, index, target.get(index), submitted.get(index),
|
||||
childPath);
|
||||
}
|
||||
}
|
||||
@@ -642,21 +644,22 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
private void overlayEditableRoomInformationValue(
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
String bookingType,
|
||||
JsonNode finalValues,
|
||||
ObjectNode parent,
|
||||
String fieldName,
|
||||
JsonNode currentValue,
|
||||
JsonNode submittedValue,
|
||||
List<String> path) {
|
||||
if (currentValue != null && currentValue.isObject()) {
|
||||
overlayEditableRoomInformationLeaves(card, bookingType, (ObjectNode) currentValue, submittedValue, path);
|
||||
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, (ObjectNode) currentValue, submittedValue, path);
|
||||
return;
|
||||
}
|
||||
if (currentValue != null && currentValue.isArray()) {
|
||||
overlayEditableRoomInformationArray(card, bookingType, (ArrayNode) currentValue, submittedValue, path);
|
||||
overlayEditableRoomInformationArray(card, bookingType, finalValues, (ArrayNode) currentValue, submittedValue, path);
|
||||
return;
|
||||
}
|
||||
if (submittedValue != null && !submittedValue.isMissingNode() && !submittedValue.isContainerNode()
|
||||
&& isRoomInformationPathWritable(card, bookingType, path)) {
|
||||
&& isRoomInformationPathWritable(card, bookingType, path, finalValues)) {
|
||||
parent.set(fieldName, submittedValue);
|
||||
}
|
||||
}
|
||||
@@ -664,21 +667,22 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
private void overlayEditableRoomInformationValue(
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
String bookingType,
|
||||
JsonNode finalValues,
|
||||
ArrayNode parent,
|
||||
int index,
|
||||
JsonNode currentValue,
|
||||
JsonNode submittedValue,
|
||||
List<String> path) {
|
||||
if (currentValue != null && currentValue.isObject()) {
|
||||
overlayEditableRoomInformationLeaves(card, bookingType, (ObjectNode) currentValue, submittedValue, path);
|
||||
overlayEditableRoomInformationLeaves(card, bookingType, finalValues, (ObjectNode) currentValue, submittedValue, path);
|
||||
return;
|
||||
}
|
||||
if (currentValue != null && currentValue.isArray()) {
|
||||
overlayEditableRoomInformationArray(card, bookingType, (ArrayNode) currentValue, submittedValue, path);
|
||||
overlayEditableRoomInformationArray(card, bookingType, finalValues, (ArrayNode) currentValue, submittedValue, path);
|
||||
return;
|
||||
}
|
||||
if (submittedValue != null && !submittedValue.isMissingNode() && !submittedValue.isContainerNode()
|
||||
&& isRoomInformationPathWritable(card, bookingType, path)) {
|
||||
&& isRoomInformationPathWritable(card, bookingType, path, finalValues)) {
|
||||
parent.set(index, submittedValue);
|
||||
}
|
||||
}
|
||||
@@ -686,7 +690,8 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
private boolean isRoomInformationPathWritable(
|
||||
ReservationV4TaskCardSnapshot card,
|
||||
String bookingType,
|
||||
List<String> path) {
|
||||
List<String> path,
|
||||
JsonNode finalValues) {
|
||||
if (path == null || path.isEmpty() || EVENT_CANCEL_BOOKING.equals(card.eventType())) {
|
||||
return false;
|
||||
}
|
||||
@@ -708,7 +713,8 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
return EVENT_NEW_BOOKING.equals(card.eventType());
|
||||
}
|
||||
if ("breakfast_included".equals(fieldName)) {
|
||||
return BOOKING_TYPE_FIT.equals(bookingType);
|
||||
JsonNode currentBreakfast = finalValues == null ? null : finalValues.path("breakfast_included");
|
||||
return BOOKING_TYPE_FIT.equals(bookingType) && isMissingOrNull(currentBreakfast);
|
||||
}
|
||||
if ("group_booking_status".equals(fieldName)) {
|
||||
return BOOKING_TYPE_GROUP.equals(bookingType);
|
||||
@@ -1739,7 +1745,8 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
throw error(HttpStatus.BAD_REQUEST, "V4_REVIEW_POINTER_READONLY", "该复核字段为只读字段,不允许修改。");
|
||||
}
|
||||
String bookingType = textAt(confirmedPayload.path("room_information"), "booking_type");
|
||||
if (!isRoomInformationPathWritable(card, bookingType, finalValuePath)) {
|
||||
JsonNode finalValues = confirmedPayload.path("room_information").path("final_values");
|
||||
if (!isRoomInformationPathWritable(card, bookingType, finalValuePath, finalValues)) {
|
||||
throw error(HttpStatus.BAD_REQUEST, "V4_REVIEW_POINTER_NOT_ALLOWED", "复核字段不在当前 Room Information 卡允许编辑字段内。");
|
||||
}
|
||||
}
|
||||
@@ -1829,6 +1836,10 @@ public class ReservationV4CommandServiceImpl implements ReservationV4CommandServ
|
||||
ObjectNode confirmedPayload,
|
||||
String pointer,
|
||||
JsonNode current) {
|
||||
if (isStableRoomInformationPayload(confirmedPayload)
|
||||
&& pointer.startsWith("/room_information/final_values/")) {
|
||||
return true;
|
||||
}
|
||||
Set<String> validationPointers = collectValidationErrorPointers(card.validationErrorsJson());
|
||||
if (validationPointers.contains(pointer)) {
|
||||
return true;
|
||||
|
||||
@@ -88,6 +88,8 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
private static final String BOOKING_TYPE_GROUP = "GROUP";
|
||||
private static final String BOOKING_TYPE_FIT = "FIT";
|
||||
private static final String LOCATOR_TYPE_GROUP_CODE = "GROUP_CODE";
|
||||
private static final String WRITE_TARGET_CONFIRMED_PAYLOAD = "confirmed_payload";
|
||||
private static final String WRITE_TARGET_REVIEW_FIELD_OVERRIDES = "review_resolution.field_overrides";
|
||||
private static final String GROUP_BOOKING_STATUS_TEN = "TEN";
|
||||
private static final Map<String, String> GROUP_BOOKING_STATUS_LABELS = Map.of(
|
||||
"TEN", "TEN-Tentative",
|
||||
@@ -397,7 +399,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
ReservationV4ActionAvailabilityResult orderAvailability,
|
||||
ReservationV4TaskCardSnapshot basicCard) {
|
||||
JsonNode displayPayload = displayPayloadForCard(orderTask, card);
|
||||
JsonNode confirmedPayload = parseJson(card.confirmedPayloadJson());
|
||||
JsonNode confirmedPayload = confirmedPayloadForCard(card);
|
||||
JsonNode reviewResolution = parseJson(card.reviewResolutionJson());
|
||||
JsonNode validationErrors = parseJson(card.validationErrorsJson());
|
||||
ReservationV4ActionAvailabilityResult availability = cardAvailability(card, orderAvailability, basicCard);
|
||||
@@ -429,6 +431,9 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
ReservationV4OrderTaskSnapshot orderTask,
|
||||
ReservationV4TaskCardSnapshot card) {
|
||||
JsonNode displayPayload = parseJson(card.displayPayloadJson());
|
||||
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
|
||||
return safeBasicInformationPayload(displayPayload);
|
||||
}
|
||||
if (!isRoomInformationEventCard(card) || !displayPayload.isObject()) {
|
||||
return displayPayload;
|
||||
}
|
||||
@@ -443,6 +448,52 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return safePayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成卡片安全确认 payload;Basic Information 不向前端暴露 Agent target_order 或其它定位原文。
|
||||
*/
|
||||
private JsonNode confirmedPayloadForCard(ReservationV4TaskCardSnapshot card) {
|
||||
JsonNode confirmedPayload = parseJson(card.confirmedPayloadJson());
|
||||
if (ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType())) {
|
||||
return safeBasicInformationPayload(confirmedPayload);
|
||||
}
|
||||
return confirmedPayload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清洗 Basic Information 展示 / 确认 payload,只保留前端需要的订单级基础字段。
|
||||
*/
|
||||
private JsonNode safeBasicInformationPayload(JsonNode payload) {
|
||||
if (payload == null || !payload.isObject()) {
|
||||
return payload == null ? NullNode.getInstance() : payload;
|
||||
}
|
||||
ObjectNode safePayload = objectMapper.createObjectNode();
|
||||
safePayload.put("card_type", ReservationV4CardType.BASIC_INFORMATION.name());
|
||||
String orderRef = textAt(payload, "order_ref");
|
||||
if (hasText(orderRef)) {
|
||||
safePayload.put("order_ref", orderRef);
|
||||
}
|
||||
JsonNode source = payload.path("basic_information").isObject()
|
||||
? payload.path("basic_information")
|
||||
: payload;
|
||||
ObjectNode basicInformation = objectMapper.createObjectNode();
|
||||
copySafeBasicInformationField(source, basicInformation, "account_code");
|
||||
copySafeBasicInformationField(source, basicInformation, "account_name");
|
||||
copySafeBasicInformationField(source, basicInformation, "market_code");
|
||||
copySafeBasicInformationField(source, basicInformation, "source_code");
|
||||
copySafeBasicInformationField(source, basicInformation, "manual_review");
|
||||
copySafeBasicInformationField(source, basicInformation, "missing_fields");
|
||||
safePayload.set("basic_information", basicInformation);
|
||||
return safePayload;
|
||||
}
|
||||
|
||||
private void copySafeBasicInformationField(JsonNode source, ObjectNode target, String fieldName) {
|
||||
JsonNode value = source == null ? null : source.get(fieldName);
|
||||
if (value == null || value.isMissingNode() || value.isNull()) {
|
||||
return;
|
||||
}
|
||||
target.set(fieldName, value);
|
||||
}
|
||||
|
||||
private ReservationV4TaskCardResult toSourceNotificationCard(
|
||||
ReservationV4SourceNotificationSnapshot notification,
|
||||
ReservationV4ActionAvailabilityResult availability) {
|
||||
@@ -1019,6 +1070,11 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
ObjectNode currentValues = currentRoomInformationProjection(orderTask, card);
|
||||
ObjectNode proposedValues = proposedRoomInformationValues(eventType, bookingType, locatorType, locatorValue, displayPayload);
|
||||
ObjectNode finalValues = finalRoomInformationValues(eventType, bookingType, locatorType, locatorValue, currentValues, proposedValues);
|
||||
ObjectNode confirmedFinalValues = confirmedStableRoomInformationFinalValues(card);
|
||||
if (confirmedFinalValues != null) {
|
||||
normalizeRoomInformationDerivedFields(bookingType, confirmedFinalValues);
|
||||
finalValues = roomInformationDisplayValues(bookingType, confirmedFinalValues, true);
|
||||
}
|
||||
|
||||
ObjectNode model = objectMapper.createObjectNode();
|
||||
model.put("event_type", eventType);
|
||||
@@ -1031,6 +1087,12 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
return model;
|
||||
}
|
||||
|
||||
private ObjectNode confirmedStableRoomInformationFinalValues(ReservationV4TaskCardSnapshot card) {
|
||||
JsonNode payload = parseJson(card.confirmedPayloadJson());
|
||||
JsonNode finalValues = payload.path("room_information").path("final_values");
|
||||
return finalValues.isObject() ? ((ObjectNode) finalValues).deepCopy() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从同一本地订单更早已确认 Room Information 卡中提取当前订单投影。
|
||||
*/
|
||||
@@ -1334,7 +1396,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
true,
|
||||
"select",
|
||||
availability.reviewable() ? "manual_review_only" : "confirm",
|
||||
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
|
||||
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
|
||||
"reservation_v4_account_catalog",
|
||||
false,
|
||||
validationMessages(validationErrors, "/basic_information/account_code", "basic_information.account_code"),
|
||||
@@ -1462,7 +1524,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
required,
|
||||
controlType,
|
||||
availability.reviewable() ? "manual_review_only" : "confirm",
|
||||
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
|
||||
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
|
||||
optionsSource,
|
||||
false,
|
||||
validationMessages(validationErrors, pointer, fieldPath),
|
||||
@@ -1491,7 +1553,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
required,
|
||||
controlType,
|
||||
availability.reviewable() ? "manual_review_only" : "confirm",
|
||||
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
|
||||
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
|
||||
optionsSource,
|
||||
false,
|
||||
validationMessages(validationErrors, pointer, fieldPath),
|
||||
@@ -1506,13 +1568,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
if (!availability.editable()) {
|
||||
return false;
|
||||
}
|
||||
if (!availability.reviewable()) {
|
||||
return true;
|
||||
}
|
||||
if (validationErrorPointers(validationErrors).contains(pointer)) {
|
||||
return true;
|
||||
}
|
||||
return isUnresolvedReviewLeaf(currentValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Set<String> validationErrorPointers(JsonNode validationErrors) {
|
||||
@@ -1641,7 +1697,7 @@ public class ReservationV4QueryServiceImpl implements ReservationV4QueryService
|
||||
false,
|
||||
controlType(fieldPath),
|
||||
availability.reviewable() ? "manual_review_only" : "confirm",
|
||||
availability.reviewable() ? "review_resolution.field_overrides" : "confirmed_payload_json",
|
||||
availability.reviewable() ? WRITE_TARGET_REVIEW_FIELD_OVERRIDES : WRITE_TARGET_CONFIRMED_PAYLOAD,
|
||||
optionsSource(fieldPath),
|
||||
false,
|
||||
fieldValidationErrors,
|
||||
|
||||
@@ -2,8 +2,10 @@ 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.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -228,6 +230,18 @@ class ReservationV4CommandControllerTest {
|
||||
org.assertj.core.api.Assertions.assertThat(confirmedPayload)
|
||||
.contains("\"group_booking_status\":\"DEF\"")
|
||||
.contains("\"group_booking_status_label\":\"DEF-Definite\"");
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}",
|
||||
roomInformation.orderTask().id()).param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.group_booking_status")
|
||||
.value("DEF"))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.group_booking_status")
|
||||
.value("DEF"));
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}/audits",
|
||||
roomingList.orderTask().id()).param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items[?(@.action=='V4_ROOMING_LIST_AUTO_DEF')].after_snapshot.update_status")
|
||||
.value(contains("UPDATED")));
|
||||
assertAuditCount("V4_ROOMING_LIST_AUTO_DEF", "v4-command-admin",
|
||||
roomInformation.businessCard().id().toString(), "\"update_status\":\"UPDATED\"", 1);
|
||||
}
|
||||
@@ -980,6 +994,10 @@ class ReservationV4CommandControllerTest {
|
||||
"version": 0,
|
||||
"reason": "按稳定 Room Information 模型修正房型",
|
||||
"field_overrides": [
|
||||
{
|
||||
"field_pointer": "/room_information/final_values/arrival_date",
|
||||
"value": "2026-08-02"
|
||||
},
|
||||
{
|
||||
"field_pointer": "/room_information/final_values/room_items/0/room_type_code",
|
||||
"value": "RM2"
|
||||
@@ -990,10 +1008,14 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.room_items[0].room_type_code")
|
||||
.value("RM2"))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.arrival_date")
|
||||
.value("2026-08-02"))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.room_information.final_values.nights")
|
||||
.value(2))
|
||||
.value(1))
|
||||
.andExpect(jsonPath("$.business_cards[0].confirmed_payload.business_fields").doesNotExist())
|
||||
.andExpect(jsonPath("$.business_cards[0].review_resolution.field_overrides[0].field_pointer")
|
||||
.value("/room_information/final_values/arrival_date"))
|
||||
.andExpect(jsonPath("$.business_cards[0].review_resolution.field_overrides[1].field_pointer")
|
||||
.value("/room_information/final_values/room_items/0/room_type_code"));
|
||||
}
|
||||
|
||||
@@ -1049,6 +1071,67 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_READONLY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRoomInformationReviewWhenFitBreakfastIsDerivedFromRateCode() throws Exception {
|
||||
seedRateCode(HOTEL_ID, 990000000000070199L, "FIT-RB");
|
||||
SeededOrderTask seeded = seedReviewOrderTaskWithBusinessCard(
|
||||
HOTEL_ID,
|
||||
"mail-v4-command-review-room-info-derived-breakfast-001",
|
||||
Instant.parse("2026-07-19T01:22:08Z"),
|
||||
990000000000070106L,
|
||||
ReservationV4CardStatus.PENDING_CONFIRM.name(),
|
||||
ReservationV4CardStatus.REVIEW_REQUIRED.name(),
|
||||
"""
|
||||
{
|
||||
"card_type":"ROOM_INFORMATION",
|
||||
"event_type":"NEW_BOOKING",
|
||||
"target_order":{"booking_type":"FIT","locator_type":"BOOKING_CODE","locator_value":"FIT-V4-REVIEW-RI-001"},
|
||||
"business_fields":{
|
||||
"event_type":"NEW_BOOKING",
|
||||
"guest_name":"FIT REVIEW",
|
||||
"arrival_date":"2026-08-01",
|
||||
"departure_date":"2026-08-03",
|
||||
"rate_code":"FIT-RB",
|
||||
"room_items":[{"room_type_code":"TWN","room_count":1}]
|
||||
}
|
||||
}
|
||||
""",
|
||||
"""
|
||||
[
|
||||
{
|
||||
"field_path": "room_information.final_values.room_items.0.room_type_code",
|
||||
"field_pointer": "/room_information/final_values/room_items/0/room_type_code",
|
||||
"message": "人工复核测试",
|
||||
"detail": "room_information.final_values.room_items.0.room_type_code: 人工复核测试。"
|
||||
}
|
||||
]
|
||||
""");
|
||||
confirmBasicCard(seeded);
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", seeded.orderTask().id()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.business_cards[0].display_payload.room_information.final_values.breakfast_included")
|
||||
.value(true))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/breakfast_included')]")
|
||||
.doesNotExist());
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), post(
|
||||
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
|
||||
seeded.orderTask().id(),
|
||||
seeded.businessCard().id())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"version": 0,
|
||||
"field_overrides": [
|
||||
{"field_pointer": "/room_information/final_values/breakfast_included", "value": false}
|
||||
]
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("V4_REVIEW_POINTER_NOT_ALLOWED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectReviewResolutionWhenResolvedOrderTaskRebindsToDifferentOrder() throws Exception {
|
||||
Long currentOrderId = 990000000000070111L;
|
||||
@@ -1577,6 +1660,20 @@ class ReservationV4CommandControllerTest {
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
private void seedRateCode(String hotelId, Long id, String code) {
|
||||
jdbcTemplate.update(
|
||||
"""
|
||||
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
|
||||
) VALUES (?, ?, 'RATE_CODE', ?, ?, 'ACTIVE', 'SYSTEM_MANAGED', NULL,
|
||||
990, 'test-review-derived-breakfast-v1', NULL, '{"pricing_available":false}', 0,
|
||||
'2026-07-19 00:00:00.000000', '2026-07-19 00:00:00.000000', NULL, NULL)
|
||||
""",
|
||||
id, hotelId, code, code);
|
||||
}
|
||||
|
||||
private void resolveBusinessReviewCard(SeededOrderTask seeded) throws Exception {
|
||||
performAuthorized(mockMvc, adminToken(), post(
|
||||
"/api/reservation/order-tasks/{orderTaskId}/cards/{cardId}/review-resolution",
|
||||
|
||||
@@ -429,7 +429,7 @@ class ReservationV4QueryControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOnlyMarkRoomInformationReviewProblemFieldsEditable() throws Exception {
|
||||
void shouldMarkRoomInformationReviewBusinessWhitelistFieldsEditable() throws Exception {
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessPayload(
|
||||
"mail-v4-room-info-review-fields-001",
|
||||
Instant.parse("2026-07-18T03:02:30Z"),
|
||||
@@ -470,11 +470,60 @@ class ReservationV4QueryControllerTest {
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/room_items/0/room_type_code')].editable")
|
||||
.value(contains(true)))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/arrival_date')].editable")
|
||||
.value(contains(false)))
|
||||
.value(contains(true)))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/departure_date')].editable")
|
||||
.value(contains(false)))
|
||||
.value(contains(true)))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/rate_code')].editable")
|
||||
.value(contains(false)));
|
||||
.value(contains(true)))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/rate_code')].write_target")
|
||||
.value(contains("review_resolution.field_overrides")))
|
||||
.andExpect(jsonPath("$.business_cards[0].fields[?(@.field_pointer=='/room_information/final_values/nights')]")
|
||||
.doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExposeBasicInformationTargetOrderInDisplayOrConfirmedPayload() throws Exception {
|
||||
ReservationV4OrderTaskSnapshot orderTask = seedOrderTaskWithBusinessPayload(
|
||||
"mail-v4-query-basic-target-order-safe-001",
|
||||
Instant.parse("2026-07-18T03:02:40Z"),
|
||||
"GROUP",
|
||||
"GROUP_CODE",
|
||||
"GRP-V4-BASIC-SAFE-001",
|
||||
"NEW_BOOKING",
|
||||
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}]}
|
||||
""",
|
||||
null);
|
||||
ReservationV4TaskCardSnapshot basicCard = workflowRepository.findTaskCardsByOrderTaskId(HOTEL_ID, orderTask.id())
|
||||
.stream()
|
||||
.filter(card -> ReservationV4CardType.BASIC_INFORMATION.name().equals(card.cardType()))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
workflowRepository.confirmTaskCardWithVersion(
|
||||
HOTEL_ID,
|
||||
basicCard.id(),
|
||||
basicCard.version(),
|
||||
"""
|
||||
{
|
||||
"basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"},
|
||||
"target_order":{"locator_value":"SHOULD-NOT-LEAK-CONFIRMED"}
|
||||
}
|
||||
""",
|
||||
"v4-query-admin",
|
||||
LocalDateTime.ofInstant(Instant.parse("2026-07-18T03:02:50Z"), ZoneOffset.UTC));
|
||||
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/order-tasks/{orderTaskId}", orderTask.id())
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.basic_information_card.display_payload.basic_information.account_code")
|
||||
.value("QBD_TRAVEL"))
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.basic_information.account_code")
|
||||
.value("QBD_TRAVEL"))
|
||||
.andExpect(jsonPath("$.basic_information_card.display_payload.target_order").doesNotExist())
|
||||
.andExpect(jsonPath("$.basic_information_card.confirmed_payload.target_order").doesNotExist())
|
||||
.andExpect(content().string(not(containsString("SHOULD-NOT-LEAK"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1024,7 +1073,12 @@ class ReservationV4QueryControllerTest {
|
||||
""");
|
||||
insertCard(orderTask, ReservationV4CardType.BASIC_INFORMATION.name(), null, 0, 20,
|
||||
basicCardStatus, reviewStatusFor(basicCardStatus), """
|
||||
{"card_type":"BASIC_INFORMATION","order_ref":"order-1","basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"}}
|
||||
{
|
||||
"card_type":"BASIC_INFORMATION",
|
||||
"order_ref":"order-1",
|
||||
"target_order":{"locator_value":"SHOULD-NOT-LEAK-DISPLAY"},
|
||||
"basic_information":{"account_code":"QBD_TRAVEL","market_code":"LEISURE","source_code":"TRAVEL_AGENT"}
|
||||
}
|
||||
""");
|
||||
ReservationV4TaskCardSnapshot businessCard = insertCard(
|
||||
orderTask,
|
||||
|
||||
Reference in New Issue
Block a user