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_idnarrow_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.mdaccept path 的 wallet / TapPay prime / transaction / payment error triage
queue_and_log.mddownstream event、queue-or-log 判讀、notification template
staging_cases.mdstaging 實測紀錄與曾經造成誤判的案例
testing.mdPHPUnit 覆蓋、後續測試建議、DB 查詢規則

Legacy 主流程

  1. 驗 request 與 API session。
  2. 從 session 解析 provider user_id
  3. 讀取 input:
    • narrow_status_id:web-app 正常流量來自 path named param narrow_status_id:<id>
    • reason:FormData post,reject path 才送
    • prime:FormData post,accept path 有 TapPay prime 時才送
  4. 呼叫 QuoteBid::processProviderAcceptNarrowMatch()
  5. ClientEventLog
    • accept:new_leads_accept
    • reject:new_leads_reject
  6. 成功回:
{
  "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_idpath rawParams[0],fallback quote_bid_id param/postint必須大於 0
narrow_status_idpath named param narrow_status_id:<id>;新 endpoint 另保留 post fallbackintWAITINGaccept 用 ACCEPTED=2,reject 用 REJECTED=3
reasonFormData poststring/nullnullreject 直接寫 activity param1;accept 前端不送 reason
primeFormData poststring/nullnullaccept 時若使用 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 規則

  1. 驗 API session;失敗回 error=1,message key 為 err_provider_accept_narrow_match_1,response status 為翻譯後文字。
  2. 解析 quote_bid_id;缺少或小於等於 0 回 error=1
  3. 讀取 narrow_status_idreasonprime
  4. 呼叫 QuoteBid::processProviderAcceptNarrowMatch()
  5. domain method 成功後才寫 ClientEventLog
    • narrow_status_id = ACCEPTEDnew_leads_accept
    • narrow_status_id = REJECTEDnew_leads_reject
  6. 成功回 { "status": "success", "error": 0 }
  7. PRO360Exception 回 HTTP 200,body 保留 legacy error / translated status / extends / bank_result_code / bank_result_msg

Model guard 順序

processProviderAcceptNarrowMatch() 會先抓 bid/request/provider,再依序檢查:

  1. bid 存在。
  2. session user 必須等於 quote_bids.provider_user_id
  3. request 不可 archived / closed / expired / hired。
  4. is_relatedErrorCardContactChargeErrorCardAutoQuote 時,legacy 會把 expired 視為可操作;新實作以同等條件略過 expired guard。
  5. quote_bids.is_narrow_match = 1
  6. quote_bids.quote_status_id = Send
  7. quote_bids.narrow_status_id = WAITING
  8. 若輸入 narrow_status_id = REJECTED,走 reject path 並直接 return,不做 qualification / payment / notification。
  9. 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 = reason

Accept path 規則

Accept 表示 provider 接受 consumer 的 narrow match invitation,會切進正式 contact / quote 主線:

  1. quote_bids.pricing_unit IS NULL,先補成空字串,避免前端讀取壞掉。
  2. 若 Redis 有 user_credit_card_just_use_<provider_user_id>,TTL 在 1 到 30 秒內時先 sleep,對齊 legacy 剛綁卡後刷卡等待。
  3. 呼叫 processWantToContactProvider($requestorUserId, $quoteBidId, $reason, true, false, null, $prime)
  4. processWantToContactProvider() 遇到 deadlock / lock wait timeout 時最多 retry 5 次。
  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()
  1. 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
  1. 建立 NarrowProviderAcceptAutoQuoteContact activity。
  2. 建立 transactions 或月結/auto quote debt 相關 transaction 狀態。
  3. 付款路徑:
    • wallet 足夠:直接扣 wallet,transaction real_pay_date = NOW()
    • wallet 不足、無既有卡、無 prime:丟內部付款錯誤,API 對外回 error=10
    • wallet 不足、有 prime:用 TapPay pay-by-prime 扣本次 auto quote debt,成功後用 TapPay rec_trade_id 標記 transaction。
    • 已有 auto quote card / auto refill:沿用既有卡與 auto quote debt 邏輯。
  4. task_event_queue.bid_contact_count
  5. event_queue.Contact_Pro
  6. 依 request 類型送 consumer notification:
    • 一般 new leads:Receive_New_Leads_Quote
    • direct reserve:Accept_Direct_Reserve_New_Leads
  7. 建立 requestor note 對話訊息。
  8. task_event_queue.request_sent_count;這個 queue 失敗只記 log,不中斷 API 主流程。
  9. 若有 quote_service_pause_events.type = WAITING_RESERVEforeign_id = quote_bid_id,改成 RESERVE 並建立 BidReserveTimeAdd activity。

accept 後 contact / 扣款 trace 詳見 payment_and_prime.md

Legacy Guard

QuoteBid::processProviderAcceptNarrowMatch() 會檢查:

規則錯誤碼message key
request invalid / session invalid1err_provider_accept_narrow_match_1
找不到 bid2err_provider_accept_narrow_match_1
呼叫者不是 provider3err_provider_accept_narrow_match_1
request 已封存 / 關閉 / 過期 / 已 hired4err_provider_accept_narrow_match_4
bid 不是 narrow match5err_provider_accept_narrow_match_1
bid 已聯繫扣款6err_provider_accept_narrow_match_6
bid 不在 waiting narrow match7err_provider_accept_narrow_match_1
provider 接案資格限制690err_provider_accept_narrow_match_690
provider 尚未完成接案資格251err_provider_accept_narrow_match_251
provider 封鎖 consumer256err_provider_accept_narrow_match_256
provider 未通過電話驗證8err_change_status_rating_6
外國卡211err_provider_accept_narrow_match_211
未設定信用卡 / charge error10err_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 user

Accept 成功資料表變化

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

也會建立:

  • transactions
  • quote_user_subscription_logs
  • quote_activities.NarrowProviderAccept
  • quote_activities.AutoQuoteContact
  • task_event_queue.bid_contact_count
  • task_event_queue.request_sent_count
  • event_queue.Contact_Pro
  • event_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_id

Reject 成功資料表變化

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 / AutoQuoteContact activity;accept 時 NarrowProviderAccept.param1=reasonAutoQuoteContact.param1=narrow_match 或來源 WantProviderQuote.param2AutoQuoteContact.param2=NULL
  • consumer notification 與 direct reserve notification

notification template 與 queue-or-log 判讀見 queue_and_log.md

新舊差異與實作注意

項目legacy新專案現況
endpointprovider_accept_narrow_match()已補
provider session guardcontroller + model 檢查已補
request closed / archived / expired / hired guardmodel 檢查已補
reject path更新 narrow_status_id = REJECTED + activity已補,PHPUnit 已覆蓋
accept path呼叫 processWantToContactProvider(),lock wait/deadlock retry已串接,PHPUnit 已覆蓋 wallet transaction / activity / queue;已補 retry
provider qualification guardBlockedUserConfig, QuoteCategoryLimitation, CertQualification已補
blocked chat user guardprovider 封鎖 consumer已補
phone verified guardprovider phone confirmed已補
consumer notificationReceive_New_Leads_Quote已補,PHPUnit 已覆蓋 email / push / sms template
direct reserve notificationReserveNotifyHelper::acceptDirectReserveNewLeads()已補等價 Accept_Direct_Reserve_New_Leads event,PHPUnit 已覆蓋
request sent counttask_event_queue.request_sent_count已補,PHPUnit 已覆蓋;queue 失敗只記 log,不中斷主流程
reserve event typeWAITING_RESERVE -> RESERVE已補,尚需 reserve fixture 實測
response extendsservice category / exception extends / bank result已補 service category、exception context、bank_result_codebank_result_msg
response statusProException::getMessageKey() 回翻譯文字已補 translated status,不回 raw message key
prime paymentchargeByCreditCard(..., $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 NULLstripe_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

資料表關聯

規則資料表欄位
找 bidquote_bidsid, provider_user_id, quote_request_id, quote_service_id
判斷 request 是否可操作quote_requestsis_archived, is_closed, is_expired, hired_count
narrow 狀態quote_bidsis_narrow_match, narrow_status_id, quote_status_id
接案資格限制blocked_user_configsuser_id, limitation fields
類別接案資格quote_category_limitations, cert_qualificationscategory / provider qualification
封鎖blocked_chat_usersapply_user_id, blocked_user_id
扣款transactionsclass = QuoteBid, foreign_id = quote_bid_id
TapPay 付款tap_pay_charge_logsuser_id, amount, status, result
活動紀錄quote_activitiesNarrowProviderAccept, NarrowProviderReject, AutoQuoteContact
預約事件quote_service_pause_eventstype, foreign_id, begin_time, end_time

測試

測試覆蓋與後續實測建議見 testing.md