narrow_match

文件狀態:已對 legacy / 已對 new API / 主要路徑已實測
最後驗證:2026-05-07
來源:legacy QuoteBidsController / QuoteBid model、new Endpoint/V1/QuoteBids.php / Lib/Model/QuoteBid.php、PHPUnit、staging curl、專案 migration detail

目的

narrow_match 是 consumer 與 provider 之間針對新客源 / 指定 provider 的雙向 invitation 流程。

它包含兩個主要 API:

APIActor目的是否扣款
/quote_bids/consumer_want_provider_quote/{quote_bid_id}.jsonconsumer / requestor邀請指定 provider 報價
/quote_bids/provider_accept_narrow_match/{quote_bid_id}/narrow_status_id:{status}.jsonprovider接受或拒絕 invitationaccept 會進 provider contact charge

這份文件是完整業務 trace 入口。單張 table 狀態細節請看:

扣款規則請看 quote_bid_contact_charge

不回答什麼

  • 不記 migration status、測試覆蓋、access log 統計。
  • 不放單次 staging quote_bid_id 案例。
  • 不重寫 TapPay / wallet / card 分支細節。

狀態模型

quote_bids.narrow_status_id 是 narrow match 主狀態:

NOT_SEND = 0
WAITING = 1
ACCEPTED = 2
REJECTED = 3

典型流程:

search_add / matching 建立 narrow bid
-> quote_bids.is_narrow_match = 1
-> quote_bids.narrow_status_id = NOT_SEND
 
consumer 邀請 provider 報價
-> consumer_want_provider_quote
-> narrow_status_id = WAITING
-> WantProviderQuote activity
-> 通知 provider
 
provider 回應 invitation
-> provider_accept_narrow_match
-> accept: narrow_status_id = ACCEPTED,並進 contact / charge 主線
-> reject: narrow_status_id = REJECTED,不扣款

Consumer 邀請 provider 報價

API:

POST /quote_bids/consumer_want_provider_quote/{quote_bid_id}.json

主流程:

consumer 呼叫 API
-> 驗 API key 與 user session
-> 確認 session user 是 quote_requests.user_id
-> 確認 request 可操作
-> 確認 bid 是 narrow match 且 narrow_status_id = NOT_SEND
-> 檢查 consumer/provider block 與 provider 可配對狀態
-> 檢查聯繫 / 邀請人數上限
-> 若帶 begin_date / end_date,寫 quote_request_reserves
-> 更新 quote_bids.narrow_status_id = WAITING
-> 寫 WantProviderQuote activity
-> 更新 provider unread
-> 通知 provider New_Leads_Pro
-> 回 success

成功後重點狀態:

quote_bids.narrow_status_id = WAITING
quote_bids.is_contact_charge = 0
quote_bids.reason = <reason>
quote_bids.requestor_note = <preserve_note 或 reserve note>
quote_activities.WantProviderQuote

如果 provider 原本先選擇不感興趣,流程會把 bid 拉回可報價狀態:

quote_bids.is_interested = 1
quote_bids.quote_status_id = Send
quote_bids.provider_status_id = Send

這支不是正式聯絡扣款流程,不要用 is_want_to_contact_provider = 1 判斷是否成功。

Provider accept / reject invitation

API:

POST /quote_bids/provider_accept_narrow_match/{quote_bid_id}/narrow_status_id:{status}.json

narrow_status_id

value行為
2accept invitation,進正式 contact / charge 主線
3reject invitation,只更新 narrow 狀態與 activity

共同前置 guard:

bid 存在
session user = quote_bids.provider_user_id
request 未 archived / closed / expired / hired
quote_bids.is_narrow_match = 1
quote_bids.quote_status_id = Send
quote_bids.narrow_status_id = WAITING

Reject

Reject 不進付款、不建 transaction、不送 contact notification。

quote_bids.narrow_status_id = REJECTED
quote_activities.NarrowProviderReject

Accept

Accept 是這個流程最容易誤判的地方:provider 接受 invitation 後,不只是把 narrow 狀態改成 accepted,而是轉進 quote bid contact charge 主線。

trace:

provider_accept_narrow_match
-> processProviderAcceptNarrowMatch
-> processWantToContactProvider
-> processWantToContactCharge
-> provider wallet / card / TapPay prime / transaction
-> activity / message / queue / consumer notification

成功後常見狀態:

quote_bids.narrow_status_id = ACCEPTED
quote_bids.quote_status_id = InProgress
quote_bids.provider_status_id = InProgress
quote_bids.quote_sent_on = NOW()
quote_bids.is_want_to_contact_provider = 1
quote_bids.contact_provider_on = NOW()
quote_bids.is_paid_for_subscription = 1
quote_bids.quote_user_subscription_log_id = transactions.id

也會建立:

quote_activities.NarrowProviderAccept
quote_activities.AutoQuoteContact
transactions.class = QuoteBid
transactions.foreign_id = quote_bid_id
task_event_queue.bid_contact_count
task_event_queue.request_sent_count

一般 new leads 會通知 consumer:

event_queue.event_key = Receive_New_Leads_Quote

direct reserve 會改送:

event_queue.event_key = Accept_Direct_Reserve_New_Leads

Direct reserve

direct reserve 判斷條件:

quote_requests.match_type = DIRECT_RESERVE
quote_requests.preserve_quote_service_id = quote_bids.quote_service_id

consumer 邀請時若帶 begin_date / end_date,會寫:

quote_request_reserves.quote_request_id
quote_request_reserves.quote_service_id
quote_request_reserves.begin_time
quote_request_reserves.end_time

並把服務日期文字併入 quote_bids.requestor_note。多段 requestor note 使用 legacy 分隔符:

#|#|#

Payment / prime

Provider accept narrow match 的付款主體是 provider,不是 consumer。

prime 是 TapPay 一次性付款 token,不是 boolean。沒有帶 prime 只代表這次 request 沒有新的 TapPay token;若 provider wallet / 既有卡不足,accept 會失敗並回要求設定信用卡類錯誤。

完整分支:

wallet 足夠 -> 直接扣 wallet
wallet 不足 + 有既有 card -> 走既有 card / auto quote debt
wallet 不足 + 無 card + 無 prime -> error,bid 不應 accepted/contacted
wallet 不足 + 有 prime -> TapPay pay-by-prime,transaction paid

詳見 quote_bid_contact_charge

第一輪排查 SQL

查 bid 與 request

SELECT
  qb.id,
  qb.quote_request_id,
  qr.user_id AS requestor_user_id,
  qb.provider_user_id,
  qb.quote_service_id,
  qb.is_narrow_match,
  qb.narrow_status_id,
  qb.quote_status_id,
  qb.provider_status_id,
  qb.is_interested,
  qb.is_contact_charge,
  qb.is_want_to_contact_provider,
  qb.contact_provider_on,
  qb.quote_sent_on,
  qb.reason,
  qb.requestor_note,
  qb.quote_user_subscription_log_id,
  qr.is_archived,
  qr.is_closed,
  qr.is_expired,
  qr.hired_count,
  qr.match_type,
  qr.preserve_quote_service_id
FROM quote_bids qb
INNER JOIN quote_requests qr ON qr.id = qb.quote_request_id
WHERE qb.id = <quote_bid_id>;

查同 request 已聯絡 / 已邀請數

SELECT
  COUNT(id) AS contact_or_invite_count
FROM quote_bids
WHERE quote_request_id = <quote_request_id>
  AND (
    (is_auto_quote = 1 AND is_want_to_contact_provider = 1)
    OR narrow_status_id IN (1, 2, 3)
  );

查 reserve

SELECT
  id,
  quote_request_id,
  quote_service_id,
  begin_time,
  end_time,
  created
FROM quote_request_reserves
WHERE quote_request_id = <quote_request_id>
ORDER BY id DESC;

查 activity

SELECT
  id,
  created,
  quote_activity_type_id,
  provider_user_id,
  requestor_user_id,
  receiver_user_id,
  secondary_foreign_id,
  param1,
  param2
FROM quote_activities
WHERE secondary_foreign_id = <quote_bid_id>
ORDER BY id DESC;

判讀原則

  • consumer 邀請 provider 報價成功,看 narrow_status_id = WAITING
  • provider reject 成功,看 narrow_status_id = REJECTED,不應有 transaction。
  • provider accept 成功,看 narrow_status_id = ACCEPTEDis_want_to_contact_provider = 1
  • provider accept 付款失敗時,bid 不應留下 accepted/contacted 狀態。
  • downstream queue 要接受 queue 或 log 任一存在。
  • 付款或 contact 異常先查 Log 與告警流程,再回查 bid / transaction。