QuoteBids Provider Accept Narrow Match
對應 legacy API POST /quote_bids/provider_accept_narrow_match/{quote_bid_id}/narrow_status_id:{narrow_status_id}.json。
注意:web-app 實際呼叫會把 quote_bid_id 與 narrow_status_id 都放在 path segment;body 是 FormData,不是 JSON。narrow_status_id 雖然在新 endpoint 仍保留 post fallback,但前端正常流量不會用 JSON body 傳它。
快速結論
- API 定位:provider 回應 consumer 送出的 narrow match invitation。
- HTTP path:
POST /quote_bids/provider_accept_narrow_match/{quote_bid_id}/narrow_status_id:{narrow_status_id}.json - 舊專案入口:
get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php::provider_accept_narrow_match($quote_bid_id) - 舊專案核心方法:
get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.php::processProviderAcceptNarrowMatch() - 新專案 endpoint:
Endpoint/V1/QuoteBids.php::provider_accept_narrow_match() - 新專案核心方法:
Lib/Model/QuoteBid.php::processProviderAcceptNarrowMatch() - 新專案可重用方法:
Lib/Model/QuoteBid.php::processWantToContactProvider() - 搬移狀態:已補 action / domain method;accept / reject / guard 已有 PHPUnit
細節文件入口
| 文件 | 用途 |
|---|---|
| payment_and_prime.md | accept path 的 wallet / TapPay prime / transaction / payment error triage |
| queue_and_log.md | downstream event、queue-or-log 判讀、notification template |
| staging_cases.md | staging 實測紀錄與曾經造成誤判的案例 |
| testing.md | PHPUnit 覆蓋、後續測試建議、DB 查詢規則 |
Legacy 主流程
- 驗 request 與 API session。
- 從 session 解析 provider
user_id。 - 讀取 input:
narrow_status_id:web-app 正常流量來自 path named paramnarrow_status_id:<id>reason:FormData post,reject path 才送prime:FormData post,accept path 有 TapPay prime 時才送
- 呼叫
QuoteBid::processProviderAcceptNarrowMatch()。 - 寫
ClientEventLog:- accept:
new_leads_accept - reject:
new_leads_reject
- accept:
- 成功回:
{
"status": "success",
"error": 0
}Request / Response 範例
Endpoint contract
路由:
POST /quote_bids/provider_accept_narrow_match/<quote_bid_id>/narrow_status_id:<narrow_status_id>.json認證:
X-PRO360-Rest-Api-Key
X-PRO360-User-Session-Token參數來源與預設:
| 參數 | 來源 | 型別 | 預設 | 規則 |
|---|---|---|---|---|
quote_bid_id | path rawParams[0],fallback quote_bid_id param/post | int | 無 | 必須大於 0 |
narrow_status_id | path named param narrow_status_id:<id>;新 endpoint 另保留 post fallback | int | WAITING | accept 用 ACCEPTED=2,reject 用 REJECTED=3 |
reason | FormData post | string/null | null | reject 直接寫 activity param1;accept 前端不送 reason |
prime | FormData post | string/null | null | accept 時若使用 TapPay 付款,送一次性付款 token;wallet 不足且需要卡付時會走 pay-by-prime |
狀態常數:
ConstNarrowMatch::NOT_SEND = 0
ConstNarrowMatch::WAITING = 1
ConstNarrowMatch::ACCEPTED = 2
ConstNarrowMatch::REJECTED = 3
ConstQuoteBidStatus::Send = 1
ConstQuoteBidStatus::InProgress = 2
ConstCategoryMatchType::DIRECT_RESERVE = 4接受 narrow match:
curl -X POST 'http://localhost:12351/quote_bids/provider_accept_narrow_match/<quote_bid_id>/narrow_status_id:2.json' \
-H 'X-PRO360-Rest-Api-Key: <api_key>' \
-H 'X-PRO360-User-Session-Token: <provider_session_token>' \
-F 'prime=<tap_pay_prime>'若接受時不需要送 TapPay prime,前端會送空的 FormData;手動測試可只保留 -X POST 與 header,不要把 narrow_status_id 改放 JSON body。
拒絕 narrow match:
curl -X POST 'http://localhost:12351/quote_bids/provider_accept_narrow_match/<quote_bid_id>/narrow_status_id:3.json' \
-H 'X-PRO360-Rest-Api-Key: <api_key>' \
-H 'X-PRO360-User-Session-Token: <provider_session_token>' \
-F 'reason=PRICE::預算不符合'前端來源:/Users/mattsu/Documents/Site/web-app/modules/utils/api-manager.js::postProRespondNewLeadsRequest()。接受時 ifAccept=true 會使用 narrow_status_id:2,只在有 prime 時 append prime;拒絕時 ifAccept=false 會使用 narrow_status_id:3,並 append reason。
成功 response:
{
"status": "success",
"error": 0
}錯誤 response 維持 HTTP 200,至少包含:
{
"error": 4,
"status": "此案件已關閉,建議您向其他案件報價",
"bank_result_code": null,
"bank_result_msg": null
}若 model 丟出的 exception 帶 context,會併入 response,例如接案資格限制:
{
"error": 690,
"status": "您的接案資格受到限制,請重新整理後再試",
"need_item": "...",
"bank_result_code": null,
"bank_result_msg": null
}status 是 legacy getMessageKey() 翻譯後文字,不是 raw message key。付款失敗若 TapPay 回傳 bank_result_code / bank_result_msg,也會放回 response,對齊 legacy controller catch。
payment / prime 細節見 payment_and_prime.md。
流程規則
Controller 規則
- 驗 API session;失敗回
error=1,message key 為err_provider_accept_narrow_match_1,responsestatus為翻譯後文字。 - 解析
quote_bid_id;缺少或小於等於 0 回error=1。 - 讀取
narrow_status_id、reason、prime。 - 呼叫
QuoteBid::processProviderAcceptNarrowMatch()。 - domain method 成功後才寫
ClientEventLog:narrow_status_id = ACCEPTED:new_leads_acceptnarrow_status_id = REJECTED:new_leads_reject
- 成功回
{ "status": "success", "error": 0 }。 PRO360Exception回 HTTP 200,body 保留 legacyerror/ translatedstatus/ extends /bank_result_code/bank_result_msg。
Model guard 順序
processProviderAcceptNarrowMatch() 會先抓 bid/request/provider,再依序檢查:
- bid 存在。
- session user 必須等於
quote_bids.provider_user_id。 - request 不可 archived / closed / expired / hired。
is_related是ErrorCardContactCharge或ErrorCardAutoQuote時,legacy 會把 expired 視為可操作;新實作以同等條件略過 expired guard。quote_bids.is_narrow_match = 1。quote_bids.quote_status_id = Send。quote_bids.narrow_status_id = WAITING。- 若輸入
narrow_status_id = REJECTED,走 reject path 並直接 return,不做 qualification / payment / notification。 - accept path 才檢查:
BlockedUserConfig::is_blocked_user()QuoteCategoryLimitation::isPass()QuoteCategoryLimitation::isStartCertBlock()+CertQualification::checkQualification()BlockedChatUser::isBlocked(provider, requestor)- provider
is_phone_confirmed
Reject path 規則
Reject 是單純拒絕 invitation,不進付款、不建交易、不送 notification:
quote_bids.narrow_status_id = REJECTED
quote_activities.quote_activity_type_id = NarrowProviderReject
quote_activities.receiver_user_id = provider_user_id
quote_activities.param1 = reasonAccept path 規則
Accept 表示 provider 接受 consumer 的 narrow match invitation,會切進正式 contact / quote 主線:
- 若
quote_bids.pricing_unit IS NULL,先補成空字串,避免前端讀取壞掉。 - 若 Redis 有
user_credit_card_just_use_<provider_user_id>,TTL 在 1 到 30 秒內時先 sleep,對齊 legacy 剛綁卡後刷卡等待。 - 呼叫
processWantToContactProvider($requestorUserId, $quoteBidId, $reason, true, false, null, $prime)。 processWantToContactProvider()遇到 deadlock / lock wait timeout 時最多 retry 5 次。- narrow accept 主線會先把 bid 轉成:
quote_bids.is_auto_quote = 1
quote_bids.narrow_status_id = ACCEPTED
quote_bids.quote_status_id = InProgress
quote_bids.provider_status_id = InProgress
quote_bids.quote_sent_on = NOW()- contact / charge 成功後會更新:
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 = quote_user_subscription_logs.id- 建立
NarrowProviderAccept和AutoQuoteContactactivity。 - 建立
transactions或月結/auto quote debt 相關 transaction 狀態。 - 付款路徑:
- wallet 足夠:直接扣 wallet,transaction
real_pay_date = NOW()。 - wallet 不足、無既有卡、無
prime:丟內部付款錯誤,API 對外回error=10。 - wallet 不足、有
prime:用 TapPaypay-by-prime扣本次 auto quote debt,成功後用 TapPayrec_trade_id標記 transaction。 - 已有 auto quote card / auto refill:沿用既有卡與 auto quote debt 邏輯。
- wallet 足夠:直接扣 wallet,transaction
- 送
task_event_queue.bid_contact_count。 - 送
event_queue.Contact_Pro。 - 依 request 類型送 consumer notification:
- 一般 new leads:
Receive_New_Leads_Quote - direct reserve:
Accept_Direct_Reserve_New_Leads
- 一般 new leads:
- 建立 requestor note 對話訊息。
- 送
task_event_queue.request_sent_count;這個 queue 失敗只記 log,不中斷 API 主流程。 - 若有
quote_service_pause_events.type = WAITING_RESERVE且foreign_id = quote_bid_id,改成RESERVE並建立BidReserveTimeAddactivity。
accept 後 contact / 扣款 trace 詳見 payment_and_prime.md。
Legacy Guard
QuoteBid::processProviderAcceptNarrowMatch() 會檢查:
| 規則 | 錯誤碼 | message key |
|---|---|---|
| request invalid / session invalid | 1 | err_provider_accept_narrow_match_1 |
| 找不到 bid | 2 | err_provider_accept_narrow_match_1 |
| 呼叫者不是 provider | 3 | err_provider_accept_narrow_match_1 |
| request 已封存 / 關閉 / 過期 / 已 hired | 4 | err_provider_accept_narrow_match_4 |
| bid 不是 narrow match | 5 | err_provider_accept_narrow_match_1 |
| bid 已聯繫扣款 | 6 | err_provider_accept_narrow_match_6 |
| bid 不在 waiting narrow match | 7 | err_provider_accept_narrow_match_1 |
| provider 接案資格限制 | 690 | err_provider_accept_narrow_match_690 |
| provider 尚未完成接案資格 | 251 | err_provider_accept_narrow_match_251 |
| provider 封鎖 consumer | 256 | err_provider_accept_narrow_match_256 |
| provider 未通過電話驗證 | 8 | err_change_status_rating_6 |
| 外國卡 | 211 | err_provider_accept_narrow_match_211 |
| 未設定信用卡 / charge error | 10 | err_provider_accept_narrow_match_10 |
可操作狀態:
quote_bids.is_narrow_match = 1
quote_bids.narrow_status_id = WAITING
quote_bids.quote_status_id = Send
quote_bids.provider_user_id = session userAccept 成功資料表變化
provider 接受時,legacy 會進入 processWantToContactProvider(),相當於由 provider 接受 invitation 後,建立正式 quote / contact 狀態。
成功後常見變化:
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 = quote_user_subscription_logs.id也會建立:
transactionsquote_user_subscription_logsquote_activities.NarrowProviderAcceptquote_activities.AutoQuoteContacttask_event_queue.bid_contact_counttask_event_queue.request_sent_countevent_queue.Contact_Proevent_queue.Receive_New_Leads_Quote
direct reserve 案件不送 Receive_New_Leads_Quote,改送:
event_queue.Accept_Direct_Reserve_New_Leads
判斷 direct reserve 的條件:
quote_requests.match_type = DIRECT_RESERVE
quote_requests.preserve_quote_service_id = quote_bids.quote_service_idReject 成功資料表變化
provider 拒絕時不進扣款流程,主要只更新 narrow 狀態:
quote_bids.narrow_status_id = REJECTED並建立:
quote_activities.quote_activity_type_id = NarrowProviderReject
quote_activities.secondary_foreign_id = quote_bid_id
quote_activities.param1 = reason不應發生:
- 不建立
transactions - 不扣 wallet / 不寫
quote_user_subscription_log_id - 不送
Contact_Pro - 不送 consumer accept notification
- 不送
bid_contact_count/request_sent_count
新專案實作
新專案已補同名 endpoint 與 domain method:
Endpoint/V1/QuoteBids.php::provider_accept_narrow_match()
Lib/Model/QuoteBid.php::processProviderAcceptNarrowMatch()accept path 重用 Lib/Model/QuoteBid.php::processWantToContactProvider() 的 narrow accept 主線。主要補齊:
- provider session / request / bid guard
- accept / reject narrow status
- contact / charge 主線串接
NarrowProviderAccept/NarrowProviderReject/AutoQuoteContactactivity;accept 時NarrowProviderAccept.param1=reason,AutoQuoteContact.param1=narrow_match或來源WantProviderQuote.param2,AutoQuoteContact.param2=NULL- consumer notification 與 direct reserve notification
notification template 與 queue-or-log 判讀見 queue_and_log.md。
新舊差異與實作注意
| 項目 | legacy | 新專案現況 |
|---|---|---|
| endpoint | 有 provider_accept_narrow_match() | 已補 |
| provider session guard | controller + model 檢查 | 已補 |
| request closed / archived / expired / hired guard | model 檢查 | 已補 |
| reject path | 更新 narrow_status_id = REJECTED + activity | 已補,PHPUnit 已覆蓋 |
| accept path | 呼叫 processWantToContactProvider(),lock wait/deadlock retry | 已串接,PHPUnit 已覆蓋 wallet transaction / activity / queue;已補 retry |
| provider qualification guard | BlockedUserConfig, QuoteCategoryLimitation, CertQualification | 已補 |
| blocked chat user guard | provider 封鎖 consumer | 已補 |
| phone verified guard | provider phone confirmed | 已補 |
| consumer notification | Receive_New_Leads_Quote | 已補,PHPUnit 已覆蓋 email / push / sms template |
| direct reserve notification | ReserveNotifyHelper::acceptDirectReserveNewLeads() | 已補等價 Accept_Direct_Reserve_New_Leads event,PHPUnit 已覆蓋 |
| request sent count | task_event_queue.request_sent_count | 已補,PHPUnit 已覆蓋;queue 失敗只記 log,不中斷主流程 |
| reserve event type | WAITING_RESERVE -> RESERVE | 已補,尚需 reserve fixture 實測 |
| response extends | service category / exception extends / bank result | 已補 service category、exception context、bank_result_code、bank_result_msg |
| response status | ProException::getMessageKey() 回翻譯文字 | 已補 translated status,不回 raw message key |
| prime payment | chargeByCreditCard(..., $prime) 走 TapPay pay-by-prime / bind card | 已補 prime 實際付款路徑 |
驗收規則
主流程 correctness
主流程以 DB state 為準,不以 downstream queue 是否已被 worker 搬走判斷:
| 情境 | 必查項目 |
|---|---|
| accept 成功 | quote_bids.narrow_status_id = ACCEPTED |
| accept 成功 | quote_bids.quote_status_id = InProgress |
| accept 成功 | quote_bids.provider_status_id = InProgress |
| accept 成功 | quote_bids.is_want_to_contact_provider = 1 |
| accept 成功 | quote_bids.quote_user_subscription_log_id > 0 或符合月結 / add transaction only 條件 |
| accept 成功 | transactions.class = QuoteBid + foreign_id = quote_bid_id |
accept 帶 prime 成功 | transaction real_pay_date IS NOT NULL 且 stripe_charge_id = TapPay rec_trade_id |
| accept 成功 | quote_activities.NarrowProviderAccept |
| accept 成功 | quote_activities.AutoQuoteContact |
| reject 成功 | quote_bids.narrow_status_id = REJECTED |
| reject 成功 | quote_activities.NarrowProviderReject |
無 prime 付款失敗 | bid 不應轉成 ACCEPTED / InProgress |
downstream event 判讀見 queue_and_log.md。payment / error triage 見 payment_and_prime.md。staging 實測案例見 staging_cases.md。
資料表關聯
| 規則 | 資料表 | 欄位 |
|---|---|---|
| 找 bid | quote_bids | id, provider_user_id, quote_request_id, quote_service_id |
| 判斷 request 是否可操作 | quote_requests | is_archived, is_closed, is_expired, hired_count |
| narrow 狀態 | quote_bids | is_narrow_match, narrow_status_id, quote_status_id |
| 接案資格限制 | blocked_user_configs | user_id, limitation fields |
| 類別接案資格 | quote_category_limitations, cert_qualifications | category / provider qualification |
| 封鎖 | blocked_chat_users | apply_user_id, blocked_user_id |
| 扣款 | transactions | class = QuoteBid, foreign_id = quote_bid_id |
| TapPay 付款 | tap_pay_charge_logs | user_id, amount, status, result |
| 活動紀錄 | quote_activities | NarrowProviderAccept, NarrowProviderReject, AutoQuoteContact |
| 預約事件 | quote_service_pause_events | type, foreign_id, begin_time, end_time |
測試
測試覆蓋與後續實測建議見 testing.md。