Quote Request Copy Implementation Plan

這份文件目前改成記錄 POST /quote_requests/copy/{quote_request_id}.json 已落地到哪裡,以及距離 legacy parity 還差什麼。

文件中若簡寫 copy.json,指的是 copy action / 流程;實際主要公開 caller 會把 source quote_request_id 放在 path segment。

目標

以 legacy parity 為目標,分批落地,但不在文件上把任何已知規則降級成 optional。

建議切法

API 入口

責任只保留:

  • 驗 session
  • quote_request_id
  • 呼叫 handler
  • 回舊格式 json

目前已完成:

  • Endpoint/V1/QuoteRequests.php::copy()
  • Lib/QuoteRequestCopy/CopyRequestHandler.php
  • Lib/QuoteRequestCopy/SourceRequestLoader.php
  • Lib/QuoteRequestCopy/FormSubmissionCloner.php
  • Lib/QuoteRequestCopy/CopyRequestFeeUpdater.php

目前已觀察到的 staging caller 形狀:

  • POST /quote_requests/copy/{id}.json
  • headers:
    • X-PRO360-Rest-Api-Key
    • X-PRO360-User-Session-Token
  • body:
    • 可為空 multipart
  • success response:
    • {"error":0,"message":"success","quote_request_id":"<new_id>"}

這代表目前主要公開 caller 至少支援:

  • source quote_request_id 走 path param
  • session flow 為主要成功路徑
  • 不依賴 request body 的必填欄位

已確認 baseline

已用 staging 成功案例 20212 -> 20213 回查 DB,以下規則已可直接視為 parity baseline:

  • quote_form_submissions 會新增一筆
  • quote_form_submission_fields 會完整複製,這筆案例是 12
  • copied request 會寫 copied_quote_request_id = 20212
  • source request 會寫 last_copied_on
  • copied request 會建立 CreateRequest activity
  • copied request 會立刻產生 auto quote 與後續 quote 活動
  • task_queue_log 會出現 action = newQuoteRequest

同時也已確認下列行為不能用簡化假設處理:

  • fee 不是沿用 source,也不是單純 category 固定 fee
  • manual_fee_discount 會重算
  • expired_on 不會直接沿用 source
  • tags 在這筆案例是 ["回客"]ConstJobCenterLabel::MULTI_REQUEST 的實際值也就是 回客

copy 專用流程

  • Lib/QuoteRequestCopy/CopyRequestHandler.php
  • Lib/QuoteRequestCopy/SourceRequestLoader.php
  • Lib/QuoteRequestCopy/FormSubmissionCloner.php

建議責任如下。

SourceRequestLoader

負責:

  • 載入來源 request
  • 驗證 owner
  • 驗證 category active
  • 驗證 reserve_type_a 是否仍可 copy
  • 載入來源 form fields

FormSubmissionCloner

負責:

  • 建立新 quote_form_submissions
  • 複製來源 quote_form_submission_fields

CopyRequestHandler

負責:

  1. blocked user 檢查
  2. 防重點擊
  3. 24h 同 category 限制
  4. 建立新 request
  5. clone submission / fields
  6. fee / summary 更新
  7. abnormal scan 與停配對補寫
  8. 未命中 abnormal 時才跑 auto quote 與 HK task
  9. 建立 CreateRequest activity
  10. 更新來源 request last_copied_on
  11. 回傳結果

實作順序

第一批:同步主線

目前已完成:

  • /quote_requests/copy/{quote_request_id}.json endpoint
  • owner / session 驗證
  • legacy parity 的 blocked user / invalid session response shape
  • hash copy 驗證
  • blocked user 檢查
  • reserve_type_a 過期檢查
  • category active 檢查
  • 24h 同 category 限制
  • Redis 防連點
  • 建立新 request
  • copied_quote_request_id
  • fee parity
  • summary parity
  • 複製 submission / fields
  • expired_on
  • last_copied_on
  • CreateRequest

仍需持續補強:

  • staging 20212 -> 20213 的欄位逐項對表
  • fee / bid 細節是否完全 parity

第二批:legacy side effects 對齊

目前已完成:

  • abnormal / keyword / overseas phone 掃描
  • auto quote dispatcher 進入主線
  • HK / area 特例 task
  • recommend source

第三批:證據補齊與 parity 驗收

  • baseline compare
  • 區域差異驗證
  • log / queue / downstream side effect 驗證

測試建議

不要沿用 Step* 命名。copy 建議獨立用規則導向命名。

目前測試檔

  • tests/QuoteRequestCopyIntegrationTest.php
  • tests/QuoteRequestCopyBaselineIntegrationTest.php
  • tests/QuoteRequestCopyAbnormalIntegrationTest.php

目前主要覆蓋:

  1. testCopiesOwnedRequestThroughRouter()
  2. testCopiesRequestSuccessfullyViaHash()
  3. testFallsBackToSessionWhenHashIsInvalid()
  4. testReturnsInvalidSessionWhenHashIsInvalidAndNoSessionExists()
  5. testReturnsInvalidRequestWhenQuoteRequestIdIsMissing()
  6. testReturnsInvalidRequestWhenSourceRequestIsNotOwned()
  7. testDispatchesAutoQuoteAfterCopy()
  8. testEnqueuesNewQuoteRequestTaskInHongKong()
  9. testWritesRecommendSourceLogWhenProvided()
  10. testCopiesBaseFeesAndExpiredOnForStableFixture()
  11. testDueDateFieldOverridesCategoryExpiredOn()
  12. tests/QuoteRequestCopyBaselineIntegrationTest::testBaselineRequestRowsMatchExpectedCopyRelationship()
  13. tests/QuoteRequestCopyBaselineIntegrationTest::testBaselineCopiedRequestHasExpectedAutoQuoteBidShape()
  14. tests/QuoteRequestCopyBaselineIntegrationTest::testBaselineCopiedRequestHasExpectedNarrowMatchBidShape()
  15. testRejectsBlockedUser()
  16. testRejectsExpiredReserveTypeARequest()
  17. testRejectsInactiveCategory()
  18. testRejectsWhenSameCategoryRequestCountReachedLimit()
  19. testPausesMatchingWhenForcedAbnormalScanFound()
  20. testPausesMatchingForHighRiskForeignPhoneScan()
  21. testReturnsSuccessButPausesMatchingForForcedOldKeywordWarning()

斷言重點

  • response error/message/quote_request_id
  • 新 request row
  • copied_quote_request_id
  • cloned submission / field count
  • last_copied_on
  • CreateRequest activity

已知風險

  1. copy() 其實混了很多 search_add 後處理

    • 如果只做同步主線,不能聲稱 parity 完成
  2. fee / expired_on 已有明確 baseline 差異

    • staging 20212 -> 20213 已證明這些欄位不是目前簡化實作可覆蓋的
    • 若不先回補 legacy 規則,API 雖然可回成功,但資料不會對
  3. allow_copy 目前下游是靠 last_copied_on

    • 這欄位若漏寫,前端顯示會不對
  4. 如果 fee / summary / auto quote / abnormal scan / newQuoteRequest task 沒對齊

    • 即使 endpoint 可以回成功,也不能算 parity 完成

目前進度

已實作並有測試或執行證據:

  • session copy
  • hash copy
  • invalid hash fallback session
  • invalid session
  • invalid request
  • cloned submission / field
  • copied_quote_request_id
  • last_copied_on
  • CreateRequest
  • fee updater 主線
  • variable fee helper 已修正
  • fee / expired_on integration test
  • due_date override integration test
  • auto quote dispatcher 進入點
  • HK newQuoteRequest task
  • recommend_source log
  • abnormal pause matching
  • foreign phone high-risk pause matching
  • old keyword forced warning 在 copy 仍回 success 但停配對
  • blocked user
  • inactive category
  • expired reserve_type_a
  • 24h same-category limit

目前 copy 相關測試集合:

  • tests/QuoteRequestCopyIntegrationTest.php
  • tests/QuoteRequestCopyAbnormalIntegrationTest.php
  • tests/QuoteRequestCopyGuardIntegrationTest.php
  • tests/QuoteRequestCopyBaselineIntegrationTest.php

目前整包執行結果:

  • docker exec -w /project-data cd63f9147e8d ./vendor/bin/phpunit tests --filter 'QuoteRequestCopy(Integration|AbnormalIntegration|GuardIntegration|BaselineIntegration)Test'
  • OK (32 tests, 288 assertions)

已確認 staging reference baseline:

  • quote_request_match_infosr / sc / tp
  • quote_bidstotal_bids = 100auto_quote_bids = 7narrow_bids = 14
  • quote_activities:type 1=12=84=2752=755=1
  • task_queue_log:存在 action = newQuoteRequestvalue = 20213
  • auto quote bid 抽樣形狀:
    • quote_status_id = 2
    • provider_status_id = 2
    • is_auto_quote = 1
    • is_narrow_match = 0
    • is_want_to_contact_provider = 0
    • total_site_fee = 111
    • pricing_unit = 小時
  • narrow match bid 抽樣形狀:
    • quote_status_id = 1
    • provider_status_id = 1
    • is_auto_quote = 0
    • is_narrow_match = 1
    • is_want_to_contact_provider = 1
    • total_site_fee = 101
    • pricing_unit = 小時
  • 已補其他真實 baseline:
    • booking baseline 14241 -> 14306
    • abnormal baseline 14691 -> 17216
    • keyword baseline 10650 -> 10652

新專案目前 matching 對齊進度:

  • 已補回 manual handler 的 read_segment >= 9 分流
  • 已補回 related general pool 對原 category service 的移除
  • 已補回 additional narrow 的 legacy exclusion 與 temp-table score boost
  • 已補回 additional narrow 同 provider / 同 score 時的穩定 tie-break
  • 已補回 narrow pool 的 self-owned service 過濾
  • 20212 / 8617 這組 reference case 目前不能再直接當成「可完全重跑的同一組 live 資料」
    • 原因不只 repeated copy / daily limit,還包含 reference provider 資料已漂移
    • 目前已確認一個實例:
      • staging reference 使用的是 provider 8085quote_service_id = 9115
      • 但 live DB 目前 9115.confirmed_prohibited_type = 2,已不符合新舊 matching 都共用的 confirmed_prohibited_type in (0, 3, 4) 條件
      • 因此現在重跑同一 source request 時,runtime 只會選到同 provider 的 quote_service_id = 9169
    • 這代表 20212 -> 20213 仍是 staging reference baseline,但不應再被表述成「今天在同一份 live DB 上一定能原樣重現」

仍需補齊或再釘更實的部分:

  • HK / area 的真實 baseline
  • 目前這份 staging DB \ProConfig::get('area') = tw,若要驗 HK 分支,需改到 HK 區域環境或另一份對應 baseline
  • HK 主線本身已補到兩層證據:
    • QuoteRequestCopyIntegrationTest.php 已用 QUOTE_REQUEST_COPY_TEST_FORCE_AREA=hknewQuoteRequest task
    • 已直接比對 legacy controller 與新 handler,area=hk!is_scan_found 的條件一致
  • 20212 -> 20213 以外更多一般 baseline 是否也都維持相同 tag 行為

後續維護方式

後續若 copy 規則、測試或 baseline 有變更,照這個順序更新文件:

  1. 先更新 rule_catalog.md 的現況與備註。
  2. 再更新 rule_details.md 的規則細節、測試 case 與 baseline。
  3. 如果新舊流程順序改變,再更新 step_by_step_compare.md
  4. 若只是新測試或新 baseline,不要把 reference baseline 寫成今天重跑一定能得到的結果。