Quote Request Copy 逐步對照
這份文件的目的不是只回答「有沒有一樣」,而是帶你從入口一路走到回傳,理解舊專案與新專案各自做了什麼。
先釐清 4906.json
POST /quote_requests/copy/4906.json在這裡是 API 路由,不是 repo 內的實體 JSON 檔。4906在舊專案代表來源quote_request_id,也就是要被複製的 request。- 舊專案入口長這樣:
QuoteRequestsController::copy($id = null, $hash = null)
- 新專案入口長這樣:
Endpoint/V1/QuoteRequests::copy()- 先從 body 或
rawParams取出quote_request_id與hash - 再交給
CopyRequestHandler::handle()
換句話說,舊版把 4906 當 path param,新版把它統一整理成 handler 的 $sourceRequestId。
一眼看懂流程
- 驗證身分
- 驗證這張來源 request 可不可以複製
- 讀來源 request 主表與 form fields
- 建立新的
quote_requests - 複製
quote_form_submissions與quote_form_submission_fields - 重算 fee / summary / expired_on
- 做異常掃描與 auto quote
- 寫 activity、更新來源 request 的
last_copied_on - 回傳 response
下面開始逐步拆。
Step 1: 入口怎麼收參數
舊專案
檔案:
做法:
copy($id = null, $hash = null)4906直接進$id- 如果 caller 有 hash,第二段 path 進
$hash
新專案
檔案:
做法:
- 先讀
rawParams - 再讀 body 的
quote_request_id與hash - body 沒帶時,fallback
rawParams[0]/rawParams[1]
理解重點:
- 這表示新版不是完全放棄舊路由,而是把 path param 轉成統一的 handler input。
- 入口層面,兩邊是相容思路,不是完全不同 API。
Step 2: 先走 hash,失敗再走 session
舊專案
檔案:
做法:
- 若有
$hash,查SecurityHash->getByHash('QuoteRequest', $hash) - 只有
foreign_id == $id才算通過 - hash 沒過,再走
RestApiHelper::hasValidApiSession($this)
新專案
檔案:
做法:
- 若
hash !== '',查SecurityHash()->getByHash('QuoteRequest', $hash) - 只有
foreign_id === $sourceRequestId才通過 - 不通過再走
ApiValidation::hasValidApiSession(...)
結論:
- 這段邏輯一致。
- 兩邊都是「hash 優先,失敗 fallback session」。
Step 3: blocked user 檢查
舊專案
檔案:
回傳:
error = 5message = Blocked
新專案
檔案:
回傳:
error = 5message = Blocked User
理解重點:
- 如果只看舊 repo controller 原始碼,會看到
Blocked。 - 但 legacy staging API 實測 response 是
Blocked User。 - 新專案目前已刻意對齊實際 legacy API 行為,不再把這點視為差異。
Step 4: 驗證來源 request 是否屬於本人
舊專案
檔案:
查詢條件:
id = $iduser_id = $user_id
新專案
檔案:
查詢條件:
id = ?user_id = ?
結論:
- 一致。
- 新版只是把查詢抽到
SourceRequestLoader。
Step 5: reserve_type_a 是否過期
舊專案
檔案:
做法:
- 查來源 request 的
quote_form_submission_fields - 找
type = reserve_type_a - 用
DateMatchUtil判斷是否過期 - 過期回:
error = 9message = 預約日期已過不可複製
新專案
檔案:
做法:
- 先把 fields 全部載入
isReserveTypeAExpired()逐筆檢查- 過期回相同 error / message
結論:
- 一致。
Step 6: category 必須是 active
舊專案
檔案:
判斷:
quote_categories.is_active == 2
新專案
檔案:
判斷:
(int)($source['category']['is_active'] ?? 0) !== 2就擋掉
結論:
- 一致。
Step 7: 防重點擊與 24 小時同 category 限制
舊專案
檔案:
做法:
- Redis key:
copy_request__{$id},TTL 20 秒 - 若超過 1 次,回
error = 4 - 再查 24 小時內同 user 同 category、未 archived、未 closed 的 request 數量
- 若
>= 3,也回error = 4
新專案
檔案:
做法:
- 同樣用
copy_request__{$sourceRequestId} - 同樣查 24 小時、同 user、同 category、未 archived、未 closed
結論:
- 一致。
Step 8: fee 基礎資料
舊專案
檔案:
做法:
- 查
quote_categories_quote_subscription_types - 若查不到,直接回:
error = 1message = no site_fee info
- 若是 booking user,全部 fee = 0,且把
match_type改成BOOKING_ADMIN_USER
新專案
檔案:
做法:
- 也是查
getFees($quoteCategoryId) - booking user 也會把 fee 歸零
- 查不到 fee 時,會回:
error = 1message = no site_fee info
結論:
- 已對齊。
Step 9: 建立新的 quote_requests
舊專案
檔案:
做法:
- 以來源 request 為基底改寫欄位
- 重要欄位包含:
site_fee/manual_fee/auto_fee/direct_fee/contact_charge_feecopied_quote_request_id = $idrequest_lang_idsend_clientblock_provider_idshas_preserve = 0is_allow_contact_charge = 0is_expired = 0tags = [ConstJobCenterLabel::MULTI_REQUEST],實際值是["回客"]
新專案
檔案:
做法:
- 建
QuoteRequestsDO - 寫入同類型欄位
- 一樣寫:
copied_quote_request_idrequest_lang_idsend_clientblock_provider_idshas_preserve = 0is_allow_contact_charge = 0is_expired = 0tags = [ConstJobCenterLabel::MULTI_REQUEST],實際值是["回客"]
理解重點:
- 核心主表欄位大方向是一致的。
- 新版建立主表前,還會先做:
hasSiteFeeInfo()resolveExpiredOn()
欄位策略補充:
- 舊版原始 select 沒有把
match_type/specific_user_tag撈進來 - 新版目前已跟著對齊:
- 一般 copy 不沿用來源
match_type - booking user 會強制
match_type = BOOKING_ADMIN_USER specific_user_tag不會從來源 request 帶到新 request
- 一般 copy 不沿用來源
block_provider_ids要分環境判讀:- legacy controller 會把 consumer 歷史已報價 provider 追加進 block list
- 新版只有在
site_environment=production時追加 - 非 production integration test 會保留 source
block_provider_ids,避免 shared DB 歷史 bid 讓測試不穩定
結論:
- production 主線已對齊;非 production 的
block_provider_ids行為是刻意穩定測試的環境差異。
Step 10: 複製 form submission 與 fields
舊專案
檔案:
做法:
- 先新增
quote_form_submissions - 再把來源 request 的
quote_form_submission_fields複製到新 request
新專案
檔案:
做法:
- 同樣先 insert
quote_form_submissions - 再逐筆 insert
quote_form_submission_fields
結論:
- 一致。
Step 11: recommend_source log
舊專案
檔案:
做法:
- 如果 request 有
recommend_source - 就寫
quote_request_recommend_logs
新專案
檔案:
做法:
- 一樣寫
quote_request_recommend_logs - 另外兼容兩種 payload:
- top-level
recommend_source QuoteRequest[recommend_source]
- top-level
結論:
- 新版至少涵蓋舊版,還多做了一層相容。
Step 12: 重新計算 variable fee、summary、expired_on
舊專案
檔案:
做法:
updateRequestVariableFee(...)reload_lang_summary(...)- 之後再依
due_date或 category 預設天數寫expired_on
新專案
檔案:
做法:
CopyRequestFeeUpdater->update(...)reloadSummary(...)expired_on在建主表前就先算好
理解重點:
- 算法方向一致。
- 只是新版把責任拆成獨立 class,而且
expired_on提前到建單時就寫入。 - 目前 code 也保留了舊版的 variable fee / discount / high value / area special case 調整邏輯。
Step 13: 異常掃描
舊專案
檔案:
做法:
- 針對海外電話、new user / keyword、category keyword 等多段邏輯判斷
- 若命中,會把 request 改成:
match_type = ABNORMAL_CHECKis_notification_mail_sent = 1
新專案
檔案:
做法:
- 改由
AbnormalScanWriter()->write(...)處理
理解重點:
- 這裡新版不是逐行搬舊碼,而是改走共用元件。
- 是否完全等價,要看
AbnormalScanWriter是否已把 copy 情境涵蓋完整。 - 從現有測試命名看,已有
QuoteRequestCopyAbnormalIntegrationTest.php幫忙守這段,但你若要做 code-level parity review,這裡仍然是重點區。
Step 14: auto quote 與 task queue
舊專案
檔案:
做法:
- 只有在這兩個條件都成立時才跑 auto quote:
origin_copied_quote_request_id是nullorigin_created > getJPLastModified($quote_category_id)
- 且必須
!$is_scan_found hk區域另外會丟newQuoteRequesttask
新專案
檔案:
做法:
- abnormal scan 沒命中時,會先檢查 legacy gate:
- 來源 request 不能本來就是 copy 出來的
- 來源 request 建立時間要晚於
getJPLastModified($quote_category_id)
- 只有 gate 通過時才跑:
AutoQuoteDispatcher()->dispatch(...)QuoteBidFinalizer()->finalize(...)
hk區域同樣在!$is_scan_found時丟newQuoteRequesttask
結論:
- 已對齊。
Step 15: activity 與 last_copied_on
舊專案
檔案:
做法:
- 建立
CreateRequestactivity - 更新來源 request 的
last_copied_on
新專案
檔案:
做法:
createRequestActivity(...)touchSourceRequest(...)
結論:
- 一致。
Step 16: 錯誤回傳 shape
舊專案
可能回傳:
error = 1, message = invalid requesterror = 1, message = no site_fee infoerror = 2, message = invalid sessionerror = 3, message = failed to create new requesterror = 4, message = failed to create form submissionerror = 6, message = failed to create form fields
新專案
檔案:
做法:
- 交易中拋例外時,現在會保留 legacy 細分:
error = 3,message = failed to create new requesterror = 4,message = failed to create form submissionerror = 6,message = failed to create form fields
結論:
- 已對齊。
整體結論
如果你的問題是「新版 copy 主流程大方向有沒有對上舊專案」:
- 有,大部分核心步驟都在
- 包含驗證、來源載入、form clone、fee 更新、summary、activity、
last_copied_on
如果你的問題是「新版是否已經和舊專案完全一致」:
- 以目前已確認的主流程、guard、fee、auto quote gate、error shape、欄位寫入策略來看,主要 parity 已經收齊
- 如果還要再追,會比較偏向零碎邊角案例,不是目前這條 copy 主線的已知大差異
目前已確認收斂的重點差異是這 4 個:
Blocked Userresponse shape- 缺 fee 設定時回
no site_fee info - auto quote 的 legacy gate
- 建單後失敗的細分 error code
目前仍要用環境條件判讀的差異:
block_provider_ids:production 追加歷史已報價 provider;非 production 保留 source 值。- HK
newQuoteRequesttask:forced-HK 測試已過,但完整區域 baseline 需要 HK 環境。 - matching 數字:
20212 -> 20213是 reference baseline,今天重跑同 source 可能受 provider 資料漂移影響。
我會怎麼解讀這些差異
- 第 1 點最後是以 legacy staging 實際回應為準
- 第 2 點與第 3 點屬於功能性差異,已補回同步
- 第 4 點屬於 error parity,現在也已補回同步
- 欄位寫入策略這輪也已補回同步
建議閱讀順序
- 先看入口與 guard
- 再看新版主流程
- 再看兩個拆出的 helper