Sync Match 與 Finalize

這份文件裡的用語先固定:

  • match
    • 指 request 建立後,如何決定配給哪個 provider,以及建立哪一種 bid
  • finalize
    • 指 bid 建立後,接著補做的收尾執行
    • 例如 contact、charge、activity、notify、receipt、reserve 後續處理

對應程式:

要分三層看

1. dispatcher

回答:

  • request 是否真的進 downstream
  • 特殊模式是否切去別的流程

2. matcher

回答:

  • provider 候選池從哪來
  • 哪些 provider 被保留或淘汰
  • 初始 quote_bids 旗標怎麼決定

3. finalizer

回答:

  • quote_status_id
  • provider_status_id
  • quote_sent_on
  • is_want_to_contact_provider
  • is_contact_charge
  • price_note
  • pricing_unit
  • quote_amount

Auto Quote Charge Sync

一般 auto quote 的同步收費鏈目前在 AutoQuoteMatcher 完成,不是在 QuoteBidFinalizer

同步階段的責任順序是:

  1. 建立 quote_bids
  2. 寫入 quote_bids.total_site_fee
  3. quote_service_categories.balance
  4. 寫入 transactions
    • transaction_type_id = 40
    • class = QuoteBid
    • foreign_id = quote_bid_id
    • real_pay_date = null
    • real_pay_amount = amount
    • is_paid_user
  5. users.first_purchase_time(首次 auto quote 付款時)

這條同步鏈對齊的 legacy 概念是:

  • QuoteBid::createAutoQuoteBid()
  • QuoteBid::logConsumeUserWallet()
  • AppModel::logQuoteBidTxn()

因此,如果看到:

  • bid 建立了
  • total_site_fee 也有值

還要再確認:

  • quote_service_categories.balance 是否同步扣除
  • transactions(40) 是否存在
  • is_paid_user / first_purchase_time 是否正確補上

Authorized Contact / Charge Sync

authorized 不是一般 auto quote loop 的尾巴,而是指定專家建 bid 後,直接進 QuoteBid::processWantToContactProvider()

前提限制:

  • authorized 不是只看 request 傳了 QuoteRequest[match_type]=2
  • request 建立時,match_type 會先依 category 規則正規化
  • 只有 category match_type = Both(3) 或 category 本身就是 Authorize(2)authorized 才是合法輸入
  • 如果 category match_type = 1,舊版會先把 request 壓回 Multiple(1),不應再進 authorized special flow
  • staging 手動驗證時,若看到 request row 最終是 match_type = 1,但後續仍跑了 authorized contact / payment,代表 current flow 仍有分流來源不一致的風險

目前同步對齊的責任順序是:

  1. AuthorizedMatchProcessor 先建立指定 quote_service_id 的 bid
  2. AuthorizedContactProcessor 交由 QuoteBid::processWantToContactProvider() 做後續
  3. 同步補 bid contact state
    • is_want_to_contact_provider
    • contact_provider_on
    • is_paid_for_subscription
  4. 同步做付款判斷
    • wallet 直接付款
    • debt only
    • immediate charge
    • auto refill package
  5. 同步建立付款紀錄與 subscription log
    • wallet path 通常新增 transactions.transaction_type_id = 33
    • auto quote card / debt path 可能新增 transactions.transaction_type_id = 40
    • quote_bids.quote_user_subscription_log_id 必須回填 quote_user_subscription_logs.id
    • 不可把 transactions.id 當成 quote_user_subscription_log_id
  6. 同步補:
    • auto_quote_contact_count
    • AutoQuoteContact
    • Contact_Pro
    • Auto_Charge_Fail
    • Auto_Charge_Pro
    • Purchase_Credit_Pro
    • 若 request 有 quote_request_life_tokensQuoteBid::processWantToContactProvider() 需送 Life55688 EVENT_EXPERT_QUOTING
  7. charge fail 時,必要時改走 fail-switch new leads
    • RetryCreditCard 若還有下一張 valid auto quote card,先 invalid 當前卡並重試下一張;全部失敗才送 Auto_Charge_Fail / fallback

這條鏈目前是 Step7 的主要驗證目標,不應再把 side effects 分散回 processor 內手補。

注意:Step8 只負責 search_add 建立 / 綁定 life_tokenquote_request_life_tokens;contact provider 成功後的 Life55688 expert quoting event 屬於 shared contact/payment method 的 side effect,完整規則以 API 路由與遷移/API Migration/pro360_api_82/api_migration/details/quote_bids/want_to_contact_provider.md 為準。

補充:

  • QuoteRequest[reason] 不決定 authorized / preserve / direct reserve 等 special flow 分流
  • special flow 仍由 match_type 與 request 結構決定
  • special flow 判斷應以 request 正規化後的 match_type 為準,不應只看原始 payload
  • reason 是 contact provider 階段的情境值,主要影響 downstream notify 行為
  • 例如 GET_PHONE_NUMBER_OF_VENDOER 會讓 Contact_Pro 額外補 app push / webpush task

Special Flow 白話表

這幾條不要混成同一種「特殊 auto quote」。

authorized

  • 指的是:request 明確指定某個 quote_service_id
  • 前提是:該 category 允許 Authorize
  • 主要入口:
    • AuthorizedMatchProcessor
    • AuthorizedContactProcessor
    • QuoteBid::processWantToContactProvider()
  • 語意:
    • 建指定 provider 的 bid
    • 直接進 contact / payment / notify / receipt 主線
  • 不走一般 broad auto quote / narrow fallback
  • 若 category 本身只允許 Multiple(1),request 即使硬送 QuoteRequest[match_type]=2,legacy 也會先正規化回 1

preserve

  • 指的是:request 要優先保留某個 provider
  • 語意:
    • 不是獨立 contact flow
    • 仍在 auto/narrow 主線內處理
    • 只是優先保住指定 quote_service_id
  • 最終可能變成:
    • is_auto_quote = 1
    • is_narrow_match = 1

direct reserve

  • 指的是:對某位 provider 發起直接預約,帶預約時段
  • 主要入口:
    • DirectReserveProcessor
  • 語意不是固定只建一筆 reserve invitation,而是兩段分流:
    • direct match success
      • 建 auto quote bid
      • finalize 後直接 contact provider
    • fail to new leads
      • 轉成 narrow / new leads
      • reserve pause event 改成 WAITING_RESERVE
  • 這條 flow 自帶:
    • reserve row
    • pause event
    • reserve activity / message / notify

轉成新客源 bid 後的通知副作用判讀:

  • DirectReserveProcessor fallback 到 new leads 時,主流程成立的核心是:
    • switchToProSearchNewLeads() 成功建立 / 切換 bid
    • quote_bids 進入 is_narrow_match = 1narrow_status_id = WAITING
    • Direct_Reserve_To_New_Leads_Pro event 成功送出
  • 這裡的「新客源 bid / new leads」是專案內部流程名詞,指的是:
    • bid 已轉成 is_narrow_match = 1
    • narrow_status_id = WAITING
    • 專家等待是否接受這筆案件
  • setMyWorkUnreadCount()sendConsumerWantProviderQuoteMail() 屬於新客源 bid 建立後的通知副作用
  • 這兩個通知副作用失敗時,現在會記 error_in_switchToProSearchNewLeads log,但不應再把已成功建立的新客源 bid 一起判成失敗
  • 所以看到:
    • bid / activity / narrow 狀態已成立
    • error_in_switchToProSearchNewLeads 有紀錄
      代表應先判讀為「主流程成功、通知副作用失敗」

direct user

  • 指的是:provider 先被放進 direct_users
  • 主要入口:
    • DirectUserProcessor
  • 語意:
    • 不是 requestor 手動指定 provider
    • 是平台依 monthly fee / match rate 規則決定是否直接送
  • 命中時會:
    • 改寫 request fee
    • 建 auto quote bid
    • direct_user_quote_request_matches
    • 再進 contact 主線

booking

  • 指的是:booking 店家或 booking admin 送出的 request
  • 主要入口:
    • BookingRequestProcessor
  • 語意:
    • 先改寫 request-side booking 資料
    • 再回一般 matcher 主線
  • auto_quote_sent_count 應由 matcher 主線直接更新,不應再靠 booking 後補 COUNT(*)

Narrow Match Pool 邊界

這次對齊過程已確認:

  • 不是所有被 auto filter 排除的 provider 都會轉成 narrow
  • legacy 只有特定來源會進 narrow pool
  • 部分 filter 在 legacy 會直接排除 provider,不會建立 narrow bid

如果看到新舊差異是「新版 narrow 數量比 legacy 多」,優先檢查:

  • AutoQuoteMatcher
  • NarrowMatchBuilder

測試入口