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:
| API | Actor | 目的 | 是否扣款 |
|---|---|---|---|
/quote_bids/consumer_want_provider_quote/{quote_bid_id}.json | consumer / requestor | 邀請指定 provider 報價 | 否 |
/quote_bids/provider_accept_narrow_match/{quote_bid_id}/narrow_status_id:{status}.json | provider | 接受或拒絕 invitation | accept 會進 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}.jsonnarrow_status_id:
| value | 行為 |
|---|---|
2 | accept invitation,進正式 contact / charge 主線 |
3 | reject 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 = WAITINGReject
Reject 不進付款、不建 transaction、不送 contact notification。
quote_bids.narrow_status_id = REJECTED
quote_activities.NarrowProviderRejectAccept
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_Quotedirect reserve 會改送:
event_queue.event_key = Accept_Direct_Reserve_New_LeadsDirect reserve
direct reserve 判斷條件:
quote_requests.match_type = DIRECT_RESERVE
quote_requests.preserve_quote_service_id = quote_bids.quote_service_idconsumer 邀請時若帶 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第一輪排查 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 = ACCEPTED且is_want_to_contact_provider = 1。 - provider accept 付款失敗時,bid 不應留下 accepted/contacted 狀態。
- downstream queue 要接受 queue 或 log 任一存在。
- 付款或 contact 異常先查 Log 與告警流程,再回查 bid / transaction。