QuoteBids Want To Contact Provider
對應 legacy API POST /quote_bids/want_to_contact_provider/{quote_bid_id}.json。
注意:quote_bid_id 是 path segment,不是 request body 欄位。實測 staging 例子:
https://api-staging.pro360.com.tw/quote_bids/want_to_contact_provider/2147688065.json快速結論
- API 定位:consumer / requestor 主動聯絡某一個已存在的 provider quote bid。
- 不是 provider 接案、不是 provider 發報價;是 consumer 對 auto quote / quote card 按下「聯絡專家」。
- HTTP path:
POST /quote_bids/want_to_contact_provider/{quote_bid_id}.json - 舊專案入口:
get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Controller/QuoteBidsController.php::want_to_contact_provider($quote_bid_id) - 新專案 endpoint:
Endpoint/V1/QuoteBids.php::want_to_contact_provider() - 新專案 routing:
Lib/Common/RouterRule/Mapping.php未設定 explicit mapping,實際走 Origin rule 到QuoteBids::want_to_contact_provider(),並由 path 取quote_bid_id - 新專案核心方法:
Lib/Model/QuoteBid.php::processWantToContactProvider() - 認證:使用者 session + API key
- 搬移狀態:已補 action;已依 legacy 修正 error 17 response、AutoQuoteContact activity 參數、
add_contact_activityoption、contact charge subscription log / transaction 判斷,已補直接 API PHPUnit coverage - payoff lock:current Legacy 沒有;PHP 8 一般 endpoint 暫時保留既有 lock,只有
search_add三個特殊 caller scoped 停用。這是 shared-method migration decision,不可誤寫成 Legacy 也有 lock。
業務流程
這支 API 的主角是 consumer,目標是開啟 consumer 與 provider 的正式洽談狀態。
- Consumer 在某個
quote_bid上按「聯絡專家」。 - API 用 session 解析 consumer
user_id。 - 檢查該 consumer 是否為
quote_bids.quote_request_id -> quote_requests.user_id的 request owner。 - 若是 auto quote 且尚未聯絡,先檢查 provider 服務時段是否仍可配合。
- 若 request 有帶
message,先寫入對話,並做 message 分類。 - 若 message 被判定為
rejected或insignificant,回{"error":17,"status":""},不進入聯絡扣款主線。 - 通過後呼叫
QuoteBid::processWantToContactProvider():- 更新
quote_bids.is_want_to_contact_provider = 1 - 記錄
contact_provider_on - 處理 site fee / subscription / transaction
- 寫 quote activity
- 送後續 queue / event
- 更新
- 成功後補
quote_conversation_with_requestor_note()。 - 回傳
{"status":"success","error":0}。
資料關聯核心:
| 規則 | 資料表 | 欄位 |
|---|---|---|
| 確認 consumer 是案件 owner | quote_requests | id, user_id |
| 找聯絡目標 provider | quote_bids | id, quote_request_id, provider_user_id, quote_service_id |
| 開啟聯絡狀態 | quote_bids | is_want_to_contact_provider, contact_provider_on |
| 統計 auto quote 聯絡數 | quote_requests | auto_quote_contact_count |
| message 分類紀錄 | quote_bid_classify_contacts | quote_bid_id, message, classes |
Legacy 主流程
- 驗 JSON request 與 API session。
- 從 session 解析 requestor
user_id。 - 讀取 input:
reasonpreserve_noteprimemessage
- 確認 auto quote bid 是否可聯繫。
- 若有
message,先做訊息檢查、分類與quote_conversation()。 - 呼叫
QuoteBid::processWantToContactProvider(...)。 - 成功後補
quote_conversation_with_requestor_note()。 - 回傳:
{
"status": "success",
"error": 0
}新專案現況
新專案已補同 action endpoint,並串接既有核心 contact provider 主線:
Lib/Model/QuoteBid.php::processWantToContactProvider()已被以下流程重用:
Lib/SearchAdd/AuthorizedContactProcessor.phpLib/SearchAdd/DirectReserveProcessor.phpLib/SearchAdd/DirectUserProcessor.php
Routing 未另外 mapping 這條帶 id 的舊路徑,會走 Origin rule:
Lib/Common/RouterRule/Mapping.php沒有quote_bids/want_to_contact_provider/{quote_bid_id}.json的 explicit mappingEndpoint/V1/QuoteBids.php已補want_to_contact_provider()
新舊程式對應
以下行號是目前 migration 實作後的定位,用來追 code review / 後續補差異;若檔案再改動,行號可能會漂移,仍以區塊職責為準。
Legacy PHP 5.6
controller:
/Users/mattsu/Documents/Site/get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Controller/QuoteBidsController.php| legacy 行號 | 職責 |
|---|---|
1830-1835 | 入口 want_to_contact_provider();檢查 JSON request 與 API session,不符合回 error=1。 |
1837-1845 | 讀 requestor user、reason、preserve_note、prime、message,初始化 model / reject flag。 |
1847-1868 | 讀 bid/request;若 auto quote 尚未聯絡,檢查服務時段;不符合時轉 new leads 並回 error=18。 |
1871-1927 | message whitelist / line id / phone / link guard;必要時呼叫 QuoteBidClassifyContact,rejected / insignificant 設為 rejected。 |
1933-1984 | 有 message 時先寫 quote_conversation(),成功後送 QuoteServiceCategory::sendAutoMessage(),deadlock / timeout 最多 retry 5 次。 |
1986-1988 | rejected message 回 error=17,status 為空字串。 |
1990-2009 | 呼叫 processWantToContactProvider(),deadlock / timeout 最多 retry 5 次。 |
2010-2021 | 成功後補 quote_conversation_with_requestor_note(),response {"status":"success","error":0}。 |
2022-2025 | ProException 走 legacy API error mapping;其他 exception 走 want_to_contact_provider_error。 |
model:
/Users/mattsu/Documents/Site/get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteBid.php| legacy 行號 | 職責 |
|---|---|
4009-4016 | 入口 processWantToContactProvider() 並讀 options;current Legacy 沒有 payoff lock。 |
4031-4056 | transaction retry;narrow match 情境先轉 auto quote,接著做 basicAutoQuoteChecking()。 |
4059-4103 | blocked / disabled / blocked chat guard;不符合回 error=690。 |
4112-4134 | 讀 provider/request;request archived 回 error=3。 |
4144-4191 | 無 subscription log 時,計算 site fee / budget / charge payload,呼叫 processCharge(),再處理 coupon / trial。 |
4193-4221 | 更新 bid:is_want_to_contact_provider=1、contact_provider_on、is_paid_for_subscription=1、is_interested=1、reason、requestor_note。 |
4224-4245 | 建立 AutoQuoteContact activity;非 narrow match 時 param1=NULL、param2=reason。 |
4253-4268 | 更新 unread count,寫 task_event_queue.bid_contact_count,commit transaction。 |
4270-4315 | 付款失敗時依 code 處理 retry card / switch to narrow / 回 error=16。 |
4318-4342 | 完成信用卡扣款與 Life55688 expert quoting side effect。 |
4344-4357 | 非 narrow / 非 direct reserve 時呼叫 sendContactProvider();notification exception 只記 log。 |
4360-4363 | 寫 processWantToContactProvider timing log;沒有 payoff lock release。 |
notification payload:
/Users/mattsu/Documents/Site/get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteBid.php::sendContactProvider()| legacy 行號 | 職責 |
|---|---|
2271-2292 | 讀 requester / provider lang / request title,建立 chat URL 短網址。 |
2294-2309 | 組 email replace content,template 為 701 Auto Quote Want to Contact Provider 或 701 (no message)Auto Quote Want to Contact Provider。 |
2311-2326 | 寫 task_event_queue.sent_contact_provider_sms,建立 email task。 |
2339-2344 | 建立 EventParameterDO,先放 email task。 |
2346-2379 | reason=GET_PHONE_NUMBER_OF_VENDOER 時補 push / web_push,target 指向 quote bid。 |
2381-2383 | 寫入 EventQueue.Contact_Pro。 |
New PHP 8.2
route / endpoint:
| new 行號 | 對應 legacy | 職責 |
|---|---|---|
2668-2687 | 1830-1835 | 入口 want_to_contact_provider();驗 session,取 quote_bid_id;id invalid 回 legacy error。 |
2689-2698 | 1837-1845 | 讀 reason、preserve_note、prime、message,初始化 model / reject flag。 |
2700-2735 | 1847-1868 | 讀 bid/request;auto quote 尚未聯絡時檢查服務時段;不符合時呼叫 legacy 對等 fallback 並回 error=18。 |
2737-2786 | 1871-1927 | message whitelist / line id / phone / link guard;必要時呼叫 QuoteBidClassifyContact,判斷 rejected。 |
2788-2837 | 1933-1984 | 有 message 時寫 quote_conversation(),成功後送 auto message,deadlock / timeout 最多 retry 5 次。 |
2839-2844 | 1986-1988 | rejected message 回 {"error":17,"status":""},避免落到 generic exception response。 |
2846-2870 | 1990-2009 | 呼叫 processWantToContactProvider(),deadlock / timeout 最多 retry 5 次。 |
2872-2883 | 2010-2021 | 成功後補 quote_conversation_with_requestor_note(),response {"status":"success","error":0}。 |
2884-2894 | 2022-2025 | PRO360Exception 走 legacy-style response:{"error":code,"status":message_key};無 explicit message key 時保留 exception message。 |
domain method:
Lib/Model/QuoteBid.php::processWantToContactProvider()| new 行號 | 對應 legacy | 職責 |
|---|---|---|
683-708 | 4009-4016 | 入口並讀 options;PHP 8 use_payoff_lock 預設 true,search_add 三個 caller 傳 false。current Legacy 沒有此 lock。 |
710-731 | 4031-4056 | 開 transaction;narrow match 情境先轉 auto quote。 |
731-745, 902-951 | 4056-4134 | basicAutoQuoteChecking()、讀 request、blocked / disabled / blocked chat guard與 error mapping。 |
751-816 | 4144-4191 | 決定 site fee,處理 subscription / wallet / credit card / transaction、coupon 與 free trial。 |
778-794 | 4193-4221 | 更新 bid:is_want_to_contact_provider=1、contact_provider_on、is_paid_for_subscription=1、is_interested=1、reason、requestor_note。 |
796-805 | 4193-4221 | 非已聯絡且非 narrow 時累加 quote_requests.auto_quote_contact_count。 |
833-857 | 4224-4245 | narrow accept activity 與 AutoQuoteContact activity;非 narrow match 時 AutoQuoteContact.param1=NULL、param2=reason;narrow match 時 NarrowProviderAccept.param1=reason,AutoQuoteContact.param1=WantProviderQuote.param2 或 narrow_match、param2=NULL。 |
861-883 | 4253-4264, 4344-4351 | 寫 bid_contact_count、my work unread count;只有非 narrow match且非 direct reserve 時,才寫 sent_contact_provider_sms 與 Contact_Pro event。 |
892-899 | 4318-4342 | contact charge 完成後,若 request 有 quote_request_life_tokens,送 Life55688 EVENT_EXPERT_QUOTING;一般非外層 transaction 呼叫時,先 commit 再送外部 side effect。 |
885-899 | 4266-4268, 4318-4342 | commit transaction、執行 transaction 後 side effect,成功回 true。 |
902-957 | 4270-4315 | charge failure code 轉 narrow / new leads,記 processWantToContactProvider log,對外回 error=16。 |
958-970 | 4344-4363 | 非 PRO360Exception log 後重拋;只有啟用 scoped option 時才釋放 PHP 8 payoff lock。 |
Payoff lock migration decision(2026-08-05)
- search_add scoped 調整 commit:
c3abd3aa。 - current Legacy
QuoteBid.php:4009-4363沒有acquirePayoffLock()/releasePayoffLock()。 - PHP 8 shared method
Lib/Model/QuoteBid.php:683-708,966-969保留預設 lock,避免直接改變既有非 search_add caller。 - 只有
AuthorizedContactProcessor.php:42-51、DirectReserveProcessor.php:116-129、DirectUserProcessor.php:123-135傳use_payoff_lock=false,對齊 search_add current Legacy。 - 一般
/quote_bids/want_to_contact_provider/{id}.json目前仍使用 PHP 8 lock;若要完全對齊該 endpoint,必須另做 scoped review,不併入 search_add 調整。 - 測試:search_add Core
3 tests / 100 assertions;非 search_add lock guard1 test / 9 assertions;本 detail 的完整 contact regression12 tests / 184 assertions。
date mismatch fallback:
Lib/Model/QuoteBid.php::switchWantToContactProviderToNarrowMatch()| new 行號 | 對應 legacy | 職責 |
|---|---|---|
4080-4097 | 4509-4526 | 用 basicAutoQuoteChecking() 讀並鎖定同一筆 auto quote bid;決定 param2=auto_quote/contact_charge 與 is_related。 |
4099-4117 | 4528-4540 | 保留同一筆 bid,改為 Send/Send、清 quote_sent_on、is_auto_quote=0、is_want_to_contact_provider=1、is_narrow_match=1、narrow_status_id=WAITING。 |
4119-4128 | 4543 | 建立 WantProviderQuote activity,receiver_user_id=requestor_user_id,param1=NULL,param2=auto_quote。 |
4130-4144 | 4545-4553 | commit / rollback;失敗時回 legacy error=16、status=err_want_to_contact_provider_16。 |
4147-4158 | 4558-4562 | 呼叫 sendConsumerWantProviderQuoteMail();新專案為避免通知外部錯誤影響 API response,通知例外只記 log。 |
notification payload:
Lib/Model/QuoteBid.php::buildContactProviderEventTask()| new 行號 | 對應 legacy | 職責 |
|---|---|---|
979-1014 | 2271-2292 | 讀 provider/requestor/request title,建立 dashboard / chat URL、security hash、dynamic short URL。 |
1015-1035 | 2294-2309, 2322-2326 | 建立 email task;template 為 701 Auto Quote Want to Contact Provider 或 701 (no message)Auto Quote Want to Contact Provider。 |
1036-1040 | 2339-2344 | 建立 EventTaskDO,top-level 保持 legacy 形狀:user_id、quote_service_id、tasks;不輸出 quote_request_id / quote_bid_id。 |
1042-1068 | 2346-2379 | reason=GET_PHONE_NUMBER_OF_VENDOER 時補 push / web_push,target 指向 quote bid。 |
1071, 829-835 | 2381-2383 | 回傳 event task,domain method 寫入 EventQueue.Contact_Pro。 |
Tests
直接 API coverage:
| test 行號 | 覆蓋內容 |
|---|---|
tests/QuoteBidsWantToContactProviderTest.php | 成功打 /quote_bids/want_to_contact_provider/{quote_bid_id}.json;驗 response、bid contact 狀態、auto_quote_contact_count、quote_user_subscription_logs、wallet 足額路徑的 transaction_type_id=33、AutoQuoteContact activity、bid_contact_count、sent_contact_provider_sms、Contact_Pro email / push event,並鎖定 event top-level 不帶 quote_request_id / quote_bid_id。 |
tests/QuoteBidsWantToContactProviderTest.php | contact 前若已存在 unrelated transaction_type_id=40 auto quote submission,仍不可跳過本次 contact charge / subscription log;測試固定 wallet 足額,因此本次 legacy 分支應新增 transaction_type_id=33 與 quote_user_subscription_logs。 |
tests/QuoteBidsWantToContactProviderTest.php | 一般 auto quote contact 在 is_check_budget=true 且本週 budget 剩餘不足時,會依 legacy QuoteServiceCategory::isBudgetNotEnough() 語意把本次 total_site_fee 降為剩餘額度、寫 UnderCharge reason、subscription log / transaction 只收剩餘額,並將 quote_service_categories.balance 歸零。 |
tests/QuoteBidsWantToContactProviderTest.php | 無 subscription log 時會重跑 getSiteFee(),不直接沿用 stale quote_bids.total_site_fee;驗 quote_bids.total_site_fee、subscription log amount、transaction amount 都使用本次重新計算 fee。 |
tests/QuoteBidsWantToContactProviderTest.php | is_contact_charge=1 時先使用 quote_bid_site_fees.fee_type=contact_charge 的 snapshot fee,再建立 subscription log / transaction。 |
tests/QuoteBidsWantToContactProviderTest.php | NewAutoQuoteTrial fee reason 成立時,成功 contact 後扣 users.free_auto_quote,並以 legacy discount 後金額寫入 bid / subscription log。 |
tests/QuoteBidsWantToContactProviderTest.php | wallet 不足、無 auto quote card、帶 prime 時,直接 /want_to_contact_provider 會用 TapPay pay-by-prime 付本次 auto quote debt;驗 transaction type 40 被標 paid、stripe_charge_id 使用 TapPay rec_trade_id、subscription log 建立但 real_pay_date 維持 legacy debt path 的 null。 |
tests/QuoteBidsWantToContactProviderTest.php | wallet 不足、無 auto quote card、帶 prime 但 TapPay 回 foreign card 時,API 回 error=211 / status=請使用國內發行的信用卡,且 transaction、subscription log、contact flag、activity、Contact_Pro 都不產生。 |
tests/QuoteBidsWantToContactProviderTest.php | 一般非 narrow want_to_contact_provider 會依 legacy 傳入 is_check_service=false,即使 provider service 後來 inactive / archived,仍可完成既有 auto quote contact;narrow accept / switch-to-new-lead 類流程仍保留 service active / archived guard。 |
tests/QuoteBidsWantToContactProviderTest.php | message 分類為 rejected 時回 {"error":17,"status":""};驗只寫 message / classify row,不產生 contact、payment、activity、Contact_Pro side effect。 |
tests/QuoteBidsWantToContactProviderTest.php | date match 失敗時回 {"error":18,"status":"err_want_to_contact_provider_18"},並走 switchWantToContactProviderToNarrowMatch();驗同 bid 轉 narrow/new lead、quote_sent_on=NULL、is_related=ErrorReserveAutoQuote、WantProviderQuote.param2=auto_quote、不扣款、不寫 Contact_Pro。 |
已存在的間接 coverage:
| test 行號 | 覆蓋內容 |
|---|---|
tests/SearchAddStep7IntegrationTest.php:553-640 | authorized search add 會呼叫 processWantToContactProvider();驗 contact_provider_on、is_paid_for_subscription、subscription log、auto_quote_contact_count、AutoQuoteContact activity、sent_contact_provider_sms、transaction、Contact_Pro email task。 |
tests/SearchAddStep7IntegrationTest.php:743-780 | reason=GET_PHONE_NUMBER_OF_VENDOER 時,驗 Contact_Pro 會帶 push / web_push tasks。 |
tests/QuoteBidsProviderAcceptNarrowMatchTest.php:234-386 | narrow accept path 會重用 contact provider 主線,驗 is_want_to_contact_provider、transaction、NarrowProviderAccept.param1=reason、AutoQuoteContact.param1=narrow_match、AutoQuoteContact.param2=NULL,並驗證不送一般 sent_contact_provider_sms / Contact_Pro。 |
tests/QuoteBidsProviderAcceptNarrowMatchTest.php | narrow accept coupon path 會套用 Narrow_Match_Use_Coupon,成功後 quote_coupons.quote_bid_id 指向本次 bid、is_fulfill=1、fulfill_on 已寫入,subscription amount 使用折扣後 fee。 |
tests/SearchAddStep7IntegrationTest.php:316-468 | direct reserve 成功 contact 會重用 contact provider 主線,驗 reserve / pause / message / Direct_Reserve_Pro,並驗證不送一般 sent_contact_provider_sms / Contact_Pro。 |
tests/SearchAddStep7IntegrationTest.php | direct reserve match 前 budget 判斷已對齊 legacy:用同 category 的 quote_service_categories.budget 與本週 auto quote transaction debt 判斷;budget 不足時直接走 new leads / Direct_Reserve_To_New_Leads_Pro,不建立 direct reserve contact。 |
tests/SearchAddStep7IntegrationTest.php:1339-1424 | direct user 命中後會重用 contact provider 主線;低 weekly balance 情境仍收 full direct user fee,且不修改 quote_service_categories.balance,對應 legacy is_check_budget=false 語意。 |
tests/SearchAddStep7IntegrationTest.php | authorized contact 遇 RetryCreditCard 且只有一張 card 時,仍回 error=16、fallback new leads、停用 auto quote card 並送 Auto_Charge_Fail。 |
tests/SearchAddStep7IntegrationTest.php | authorized contact 遇 RetryCreditCard 且還有下一張 valid auto quote card 時,會 invalid 失敗卡後重試下一張並成功完成 contact / transaction,不送 Auto_Charge_Fail。 |
Shared Method Audit Notes
processWantToContactProvider() 是多個 flow 共用的 domain method;對照 legacy 時需同時檢查 caller 傳入 options 與下游 side effect。
已修正:
is_direct_reserve與is_narrow_match通知邊界:legacy 只有非 narrow、非 direct reserve 才呼叫sendContactProvider()。新版已改為只在相同條件下寫sent_contact_provider_sms與Contact_Pro。basicAutoQuoteChecking()requester active guard:新版曾以provider_user_id查 requester user,已改回quote_requests.user_id。basicAutoQuoteChecking()service active / archived guard:legacyprocessWantToContactProvider()以$is_narrow_match傳入$is_check_service;一般非 narrow contact 不檢查 service active / archived,narrow path 才檢查。新版已補$is_check_service參數並在 contact 主線用$isNarrowMatch傳入。is_check_budget:legacy 會在非 narrow、非 contact charge 且is_check_budget=true時呼叫QuoteServiceCategory::isBudgetNotEnough(),以本週既有 auto quote transaction debt 和quote_service_categories.budget判斷是否 undercharge。新版已補等價的Transaction::getAutoQuoteTransactionThisWeek()與 contact 階段 budget limit:一般 auto quote contact 會降成 weekly budget 剩餘額並寫UnderChargereason;direct user caller 明確傳is_check_budget=false,因此維持 full fee,不修改quote_service_categories.balance。- direct reserve match 前 budget 邊界:legacy
ReserveMatchUtil在 direct match 前先呼叫QuoteServiceCategory::isBudgetNotEnough();budget 不足時直接轉 new leads,budget 足夠時更新quote_service_categories.balance後才進 direct reserve contact。新版DirectReserveProcessor已改為同樣使用本週 transaction debt + category budget 判斷,不再只看balance欄位。 add_contact_activity:legacy option 預設為 true,false 時不建立AutoQuoteContactactivity。新版已補相同 option;目前 repo 內沒有 caller 傳 false,但共用方法保留此邊界。- narrow activity 參數:legacy 先建立
NarrowProviderAccept.param1=reason,接著把$reason清成 null;AutoQuoteContact.param1取來源WantProviderQuote.param2,取不到時用narrow_match,param2=NULL。新版已補此參數語意並加入 provider accept narrow match test。 - Life55688 expert quoting side effect:legacy 在 contact charge transaction block 成功後檢查
QuoteRequestLifeToken,有 life token 時送EVENT_EXPERT_QUOTING。新版已補Life55688Util::addEventExpertQuoting()與QuoteBid::sendLife55688ExpertQuotingEvent();一般非外層 transaction 呼叫時會先 commit 再送外部 side effect。一般無 life token request 不會觸發外部 API。 - payment fee side effects:legacy 無 subscription log 時會重新跑
getSiteFee()。新版已補一般 contact fee 重算、quote_bid_site_fees.contact_chargesnapshot、coupon fulfill、NewAutoQuoteTrial扣users.free_auto_quote,並補 PHPUnit fixture。is_check_budget=false的 direct user caller 保留建 bid 時的 full fee,不套 free trial / budget undercharge,避免破壞 direct user full-fee contract。 RetryCreditCard多卡 retry:legacy 遇到 code 104 時只 invalid 當前 auto quote card;若還有下一張 valid card,while loop 會重試並可成功。新版已補 contact provider debt charge retry loop:單張卡 retry 失敗仍回error=16並送 charge fail;多張卡時會 invalid 失敗卡、保留下一張 valid card 並重試成功。
剩餘注意事項:
Contact_Proworker 發送端可能因 staging notification template / SMS provider 設定回notification template setting not found或send error,不列為 API 主流程阻塞。
Payment Parity Follow-up Matrix
payment parity 以測試先行,不先改付款主線。處理狀態:
| 狀態 | 情境 | legacy 規則 | 驗證點 |
|---|---|---|---|
| 已補 | contact charge fee snapshot | is_contact_charge=1 時先讀 quote_bid_site_fees.fee_type=contact_charge,並覆寫本次 quote_request.contact_charge_fee 後再跑 getSiteFee() | quote_bids.total_site_fee、subscription log amount、transaction amount 都使用 snapshot fee,不受 request row 後續 fee 異動影響 |
| 已補 | coupon fulfill | site_fee_reason[Coupon] 且非 contact charge 時,成功 contact 後呼叫 QuoteCoupon::fulfillCoupon() | quote_coupons.quote_bid_id 指向本次 bid,is_fulfill=1、fulfill_on 已寫入,subscription amount 使用折扣後 fee |
| 已補 | new auto quote trial | site_fee_reason[NewAutoQuoteTrial] 時,成功 contact 後 users.free_auto_quote = free_auto_quote - 1 | provider free_auto_quote 減 1,total_site_fee 使用 legacy FreeAutoQuote discount 後金額,subscription amount 對齊 |
| 已補 | existing total_site_fee > 0 | legacy 無 subscription log 時仍重新跑 getSiteFee(),不是直接沿用 bid 上既有 fee | 建立 bid total_site_fee 與 request fee 不一致的 fixture,確認本次 contact 以重新計算結果為準 |
| 已補 | direct user full fee boundary | direct user 由 caller 建 bid 決定 full fee,並傳 is_check_budget=false;不得被 weekly budget / free trial 改寫 | SearchAddStep7IntegrationTest::testCreatesDirectUserMatch 驗 quote_bids.total_site_fee = quote_requests.auto_fee = direct_fee |
| 已補 | retry next card | RetryCreditCard 後 invalid 目前卡,若還有下一張 auto quote card,while loop 重試 | SearchAddStep7IntegrationTest::testAuthorizedModeRetriesNextCardWhenRetryCreditCardFails 驗第一張卡 invalid、第二張卡成功扣款、transaction paid、未送 charge fail |
| 已補 | prime / pay-by-prime direct test | legacy 帶 prime 時會走 TapPay pay-by-prime;foreign card 失敗時不落 contact side effect | QuoteBidsWantToContactProviderTest::testConsumerCanContactProviderAndPayAutoQuoteDebtByPrimeWhenWalletInsufficient 驗 transaction type 40 paid + TapPay charge id;testConsumerContactProviderPrimeForeignCardFailureHasNoContactSideEffects 驗 error=211 與 rollback |
Staging Baseline
2026-05-19 legacy staging 成功案例:
POST /quote_bids/want_to_contact_provider/2147686291.json
reason = DEFAULT_MESSAGE
message = 有興趣,想了解更多細節
response = {"status":"success","error":0}| table | 結果 |
|---|---|
quote_bids | is_auto_quote=1、is_narrow_match=0、narrow_status_id=0、is_want_to_contact_provider=1、is_paid_for_subscription=1、contact_provider_on=2026-05-19 08:01:08、quote_user_subscription_log_id=20573、reason=DEFAULT_MESSAGE、total_site_fee=111。 |
quote_requests | auto_quote_contact_count=5;request 未 archived / closed / expired / hired。 |
transactions | 最新 transaction id=36449、transaction_type_id=33、amount=111.00、real_pay_date=2026-05-19 08:01:08。 |
quote_activities | AutoQuoteContact(50),provider_user_id=7331、requestor_user_id=8617、receiver_user_id=8617、param1=NULL、param2=DEFAULT_MESSAGE。 |
messages | 寫入 sender / receiver 兩列,內容為 有興趣,想了解更多細節。 |
quote_bid_classify_contacts | 無資料;原因是該 message 存在於 chat_can_messages 且 is_reject=0,不會進分類。 |
task_event_queue_log | bid_contact_count、sent_contact_provider_sms 成功進 log,status_id=1。 |
event_queue_log_4 | Contact_Pro email event id=56728,template 701 Auto Quote Want to Contact Provider 成功;SMS event id=56729-56732 有產生但發送端回 send error。 |
legacy Contact_Pro payload top-level 僅有:
user_id
quote_service_id
tasks新專案已調整為相同形狀;quote_bid_id 只留在 push / web_push 的 task payload 中。
新專案 staging 實測與修正
2026-05-19 新專案 staging 成功案例:
POST /quote_bids/want_to_contact_provider/2147686292.json
response = {"status":"success","error":0}
POST /quote_bids/want_to_contact_provider/2147687995.json
response = {"status":"success","error":0}兩筆 response、bid contact 狀態、AutoQuoteContact activity、messages、Contact_Pro email / SMS event 都與 legacy 主線一致,但實測時發現付款 side effect 不一致:
| bid | 實測時看到的問題 |
|---|---|
2147686292 | contact 前已有 transaction_type_id=40,新程式因此跳過本次 contact charge / subscription log 流程;成功後 quote_bids.quote_user_subscription_log_id=0。 |
2147687995 | 同樣已有 transaction_type_id=40,成功 contact 後仍沒有本次 contact 對應的 quote_user_subscription_logs,無法與 legacy 的 paid/debt lifecycle 對齊。 |
legacy staging baseline 2147686291 在 wallet 足額 contact 當下建立:
| table | legacy side effect |
|---|---|
transactions | 新增 transaction_type_id=33,amount=total_site_fee,wallet 成功扣款時有 real_pay_date。 |
quote_user_subscription_logs | 新增一筆 contact subscription log。 |
quote_bids | quote_user_subscription_log_id 指向該 subscription log。 |
2026-05-19 正式機抽樣校正:
SELECT
CASE
WHEN t.transaction_type_id = 33 THEN 'type33'
WHEN t.transaction_type_id = 40 THEN 'type40'
WHEN t.id IS NULL THEN 'no_transaction'
ELSE 'other'
END AS txn_shape,
COUNT(*) AS c,
SUM(t.real_pay_date IS NOT NULL) AS paid_cnt,
SUM(t.real_pay_date IS NULL) AS unpaid_cnt
FROM (
SELECT secondary_foreign_id AS quote_bid_id, id, created
FROM quote_activities
WHERE secondary_model = 'QuoteBid'
AND quote_activity_type_id = 50
AND created >= '2026-05-01 00:00:00'
ORDER BY id DESC
LIMIT 1000
) qa
JOIN quote_bids qb ON qb.id = qa.quote_bid_id
LEFT JOIN transactions t
ON t.class = 'QuoteBid'
AND t.foreign_id = qb.id
AND t.transaction_type_id IN (33, 40)
GROUP BY txn_shape
ORDER BY c DESC;| txn_shape | c | paid_cnt | unpaid_cnt |
|---|---|---|---|
type40 | 685 | 265 | 420 |
type33 | 315 | 315 | 0 |
正式機 baseline 因此不是「contact 一律 type33」;legacy 會依付款情境選擇:
| 情境 | transaction type | 說明 |
|---|---|---|
| wallet / 一般直接扣款 | 33 | 建立後即 paid。 |
| auto quote card / debt 路徑 | 40 | 可能已 paid,也可能先 unpaid debt。 |
重要判斷:既有 unrelated transaction_type_id=40 不能直接代表「本次 contact charge 已完成」。本次 API 成功後仍應建立 legacy 對應的 transaction_type_id=33 或 40,同時建立 quote_user_subscription_logs 並回寫 quote_bids.quote_user_subscription_log_id。
已修正的新專案對照:
| new code | legacy 對照 | 說明 |
|---|---|---|
QuoteBid::hasQuoteUserSubscriptionLog() | legacy contact 是否已完成扣款 / subscription | 只看 quote_bids.quote_user_subscription_log_id,不再把 auto quote submission 的 transaction_type_id=40 或不可靠的 quote_user_subscription_logs.quote_bid_id 當成 contact 已扣款。 |
QuoteBid::processWantToContactCharge() | legacy contact charge | contact charge 會新增對應 transaction;wallet 付款為 transaction_type_id=33,auto quote card / debt 路徑為 transaction_type_id=40,同時建立 quote_user_subscription_logs。 |
QuoteBid::updateUserSubscriptionLogId() | legacy bid 回填 subscription log | quote_bids.quote_user_subscription_log_id 回填 quote_user_subscription_logs.id,而不是 transaction id。 |
注意:staging / production schema 的 quote_user_subscription_logs.quote_bid_id 是 signed int,對超過 2147483647 的 bid 會被截成 2147483647;legacy baseline 也有這個現象。因此程式判斷與驗證都應以 quote_bids.quote_user_subscription_log_id -> quote_user_subscription_logs.id 為準,不要用 quote_user_subscription_logs.quote_bid_id 反查或判斷大 id bid。
2026-05-20 新專案 staging 實測
POST /quote_bids/want_to_contact_provider/2147688064.json
reason = DEFAULT_MESSAGE
message = 有興趣,想了解更多細節
response = {"status":"success","error":0}request session 對應 user_id=8617,request owner 也是 8617。主流程 DB 與 legacy 成功路徑一致:
| table | 結果 |
|---|---|
quote_bids | quote_request_id=21874、provider_user_id=5319、quote_service_id=3078、is_auto_quote=1、is_narrow_match=0、narrow_status_id=0、is_want_to_contact_provider=1、is_paid_for_subscription=1、is_interested=1、contact_provider_on=2026-05-20 02:22:10、quote_user_subscription_log_id=20611、reason=DEFAULT_MESSAGE、total_site_fee=111。 |
quote_requests | id=21874、auto_quote_contact_count=1;request 未 archived / closed / expired / hired。 |
quote_user_subscription_logs | id=20611、user_id=5319、amount=111、is_paid=1、payment_gateway_id=1、real_pay_date=NULL、point_rate=11;quote_bid_id=2147483647 是既有 signed int overflow,判斷仍以 quote_bids.quote_user_subscription_log_id 為準。 |
transactions | 本次 provider available_wallet_amount=44 小於 fee 111,且有有效 auto-quote card,因此走 legacy auto quote debt / card 路徑;bid 有 transaction_type_id=40 的 submission 與 contact charge 兩筆交易,皆以同一 stripe_charge_id=D20260520crOlCB 標成 paid。 |
quote_activities | 寫入 message activity quote_activity_type_id=22,以及 AutoQuoteContact(50):provider_user_id=5319、requestor_user_id=8617、receiver_user_id=8617、param1=NULL、param2=DEFAULT_MESSAGE。 |
messages | 寫入 sender / receiver 兩列,內容為 有興趣,想了解更多細節。 |
quote_bid_classify_contacts | 無資料;原因是該 message 存在於 chat_can_messages 且 is_reject=0,不會進分類。 |
task_event_queue_log | bid_contact_count、sent_contact_provider_sms 已進 log,status_id=1。 |
event_queue_log_4 | Contact_Pro 已進 log,email template 為 701 Auto Quote Want to Contact Provider,payload top-level 維持 legacy 形狀:user_id、quote_service_id、tasks。 |
本筆與 2026-05-19 legacy baseline 的 transaction type 不同是付款情境不同,不是新舊不一致:legacy wallet 足額時新增 transaction_type_id=33;wallet 不足且有 auto-quote card / debt 時,legacy 也會新增 transaction_type_id=40 並由後續 debt charge 標成 paid。
同一筆 bid 的 rejected / insignificant branch 也已驗證:
POST /quote_bids/want_to_contact_provider/2147688065.json
reason = SEND_MESSAGE
message = 你好
response = {"error":17,"status":""}quote_bid_classify_contacts 寫入 message=你好、classes=insignificant、model_version=gpt-5.2-2025-12-11、params=[{"label":"insignificant","score":0.86,"reason":"僅打招呼無其他資訊"}]。此路徑會先寫入 sender / receiver message 與 message activity,但不會進 contact 主線,不會寫 AutoQuoteContact、subscription log、transaction 或 Contact_Pro。同一筆 bid 後續改用 DEFAULT_MESSAGE 與白名單罐頭訊息 有興趣,想了解更多細節 後成功,符合 legacy:chat_can_messages.is_reject=0 的罐頭訊息會跳過 classifier。
可重用與缺口
新專案已可重用:
Lib/Model/QuoteBid.php::processWantToContactProvider()- contact provider / charge / transaction / activity / queue 主線
Lib/Model/Message.php::quote_conversation()- consumer 傳送 message
Lib/Model/Message.php::quote_conversation_with_requestor_note()- 成功聯絡後補 requestor note message
Lib/Model/QuoteServiceCategory.php::sendAutoMessage()- legacy controller 在
message成功寫入後會觸發 auto message
- legacy controller 在
Lib/Util/DateMatchUtil.php::isSingleMatch()- 已補上 legacy contact 前的預約時段 guard
Lib/Util/DateMatchUtil.php::isMatch()/isMatchTypeA()/isMatchTypeB()- 已補上
isSingleMatch()相依的底層比對規則
- 已補上
Lib/Model/QuoteBidClassifyContact.php- 已補
classifyV18()與classifyChatGPT()
- 已補
Lib/Util/ClassifyModelUtil.php- 已補 legacy BERT classifier API wrapper
Lib/Util/GPTUtil.php::send_responses_with_schema()- 已補 Responses API JSON schema helper
legacy 對應位置:
get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteBidClassifyContact.phpget-lancer-docker-2/get-lancer/app/Lib/ClassifyModelUtil.phpget-lancer-docker-2/get-lancer/app/Lib/GPTUtil.php::send_responses_with_schema()get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Controller/QuoteBidsController.php::want_to_contact_provider()
DB 已有 quote_bid_classify_contacts,欄位包含 quote_bid_id、message、classes、model_version、status、params。
資料表與 side effect
主要會影響:
quote_bidsis_want_to_contact_providercontact_provider_onis_paid_for_subscriptiontotal_site_feereasonrequestor_note
quote_requestsauto_quote_contact_count
quote_activitiesAutoQuoteContact- legacy 非 narrow match 時
param1 = NULL,param2 = reason
transactions/ subscription log
後續 queue / event 新舊對照:
| table | event_key | legacy 寫入點 | 新專案寫入點 | 新專案 worker / handler | 用途 |
|---|---|---|---|---|---|
task_event_queue | bid_contact_count | get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteBid.php::processWantToContactProvider() | Lib/Model/QuoteBid.php::processWantToContactProvider() | Endpoint/V1/TaskEventQueue.php -> BidContactCountHandler | 重算 provider / service / request 的 auto quote 或 contact charge contact counter |
task_event_queue | sent_contact_provider_sms | get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteBid.php::sendContactProvider() | Lib/Model/QuoteBid.php::processWantToContactProvider() | Endpoint/V1/TaskEventQueue.php -> SentContactProviderSMSHandler | 建 provider chat URL 與 SMS task,再送 event_queue.Contact_Pro |
event_queue | Contact_Pro | get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Model/QuoteBid.php::sendContactProvider() | Lib/Model/QuoteBid.php::processWantToContactProvider() | Endpoint/V1/EventQueue.php -> event_actions -> NotificationFactory | 依 template 設定送 email / push / web push / SMS / MoEngage |
legacy 也有 TaskEventQueue / EventQueue model:
get-lancer-docker-2/get-lancer/app/Model/TaskEventQueue.phpget-lancer-docker-2/get-lancer/app/Model/EventQueue.php
新專案的 queue consumer 較清楚集中在 Endpoint/V1/TaskEventQueue.php 與 Endpoint/V1/EventQueue.php。Contact_Pro 不是硬寫單一 handler,而是 EventQueue worker 查 event_actions.event_key = Contact_Pro,再依 template_type 交給 notification handler。
注意:測試機也可能有 cron / worker 正在跑,queue row 送出後可能很快被抓去執行並搬到 log table。驗證時不要只查原 queue table,也要查 task_event_queue_log 與 event_queue_log_*,並留意通知、counter、transaction 等 side effect 可能已經發生。
搬移風險
processWantToContactProvider() 已覆蓋 contact/charge 主線,這次補上的 endpoint 另外串接 legacy controller 層行為:
message分類與 rejected 判斷quote_conversation()quote_conversation_with_requestor_note()QuoteServiceCategory::sendAutoMessage()- retry lock timeout / deadlock
- legacy error code 與 message mapping
成功路徑、rejected / insignificant 路徑與直接 prime / TapPay pay-by-prime payment sequence 已覆蓋;staging notification worker 錯誤需依 queue/log 規則另行判讀。
Legacy Parity 注意事項
Error 17 response
legacy 在 message 被分類為 rejected 或 insignificant 時,會先寫入 message / message activity,接著回:
{
"error": 17,
"status": ""
}新專案不能讓這個錯誤落到 Router 的 PRO360Exception handler,否則 response key 會變成 message,與 legacy 不一致。
AutoQuoteContact activity
legacy QuoteBid::processWantToContactProvider() 建立 AutoQuoteContact activity 時:
| 情境 | param1 | param2 |
|---|---|---|
| 非 narrow match | NULL | reason |
| narrow match | narrow_match 或來源 WantProviderQuote.param2 | NULL |
narrow accept 的 reason 不放在 AutoQuoteContact.param2;legacy 會放在同 bid 的 NarrowProviderAccept.param1。
因此一般 /quote_bids/want_to_contact_provider/{quote_bid_id}.json 成功時,若 reason = SEND_MESSAGE,activity 應是:
quote_activity_type_id = 50
param1 = NULL
param2 = SEND_MESSAGE正式機 access log 檢查
- 機器:
ip-10-5-2-242 - 檔案:
/var/log/apache2/get-lancer_access.log* - 範圍:約
2026-04-21~2026-05-04 quote_bids/want_to_contact_provider:目前 log381筆、.1有502筆、gz 有8353筆- 排除
_for_call_jwt後:目前 log372筆、.1有491筆、gz 有8137筆 - 判斷:endpoint 有實際呼叫,搬移優先度高
2026-05-19 重新查詢:
LOG_DIR=/var/log/apache2
ACCESS_LOG=get-lancer_access.log
PATTERN='quote_bids/want_to_contact_provider/'| 條件 | 筆數 |
|---|---|
命中 /quote_bids/want_to_contact_provider/ | 8739 |
排除 want_to_contact_provider_for_call_jwt | 8740 |
排除 JWT 後只看 POST | 6386 |
8739 與 8740 差 1 應視為查詢間隔中 access log 仍在寫入,不代表 filter 異常。樣本包含 Web、iOS app、Android okhttp,皆為 POST /quote_bids/want_to_contact_provider/{quote_bid_id}.json,HTTP status 200。
結論:這支不是邊緣流量,consumer_want_provider_quote staging 驗證完成後,下一支應優先同步 / 實測 want_to_contact_provider。
Log Triage
這支若失敗,優先查 application log:
channel = processWantToContactProviderchannel = logQuoteBidTxn
常見判讀:
response error只代表對外訊息- 真正付款、guard、transaction 失敗原因要以 log channel 為準
實測準備
這支會改 DB、可能扣款或產生 transaction,不要隨便拿正式資料直接測。建議先用本機 / 測試 DB,並挑明確可回復的測試 bid。
2026-05-19 接續策略:
consumer_want_provider_quotestaging 驗證已完成,下一支同步這支 API。- 優先驗 response、
quote_bids、quote_requests.auto_quote_contact_count、quote_activities.AutoQuoteContact、transactions/ application log。 Contact_Pro/ SMS worker retry 先只記錄,不作為本輪阻塞;若 Every8d 或 SMS provider 回錯,另走外部 SMS 發送切換文件處理。
找 API key
SELECT id, name, client, version, is_active
FROM api_keys
WHERE is_active = 1
ORDER BY id DESC
LIMIT 20;curl header 使用 api_keys.name:
X-PRO360-Rest-Api-Key: <api_keys.name>找可測 quote bid
成功路徑優先找 auto quote、尚未聯絡、request 未關閉的 bid:
SELECT
b.id AS quote_bid_id,
b.quote_request_id,
r.user_id AS requestor_user_id,
b.provider_user_id,
b.quote_service_id,
b.is_auto_quote,
b.is_contact_charge,
b.is_want_to_contact_provider,
r.is_archived,
r.is_closed,
r.is_expired
FROM quote_bids b
INNER JOIN quote_requests r ON r.id = b.quote_request_id
WHERE b.is_auto_quote = 1
AND b.is_contact_charge = 0
AND b.is_want_to_contact_provider = 0
AND r.is_archived = 0
AND r.is_closed = 0
AND COALESCE(r.is_expired, 0) = 0
ORDER BY b.id DESC
LIMIT 20;找 requestor session
SELECT id, user_id, device_id, api_key, expires
FROM api_sessions
WHERE user_id = <requestor_user_id>
ORDER BY expires DESC
LIMIT 10;curl header 使用 api_sessions.id:
X-PRO360-User-Session-Token: <api_sessions.id>Curl
以下用本機 URL 示範;staging / production 只需換 host,path 仍是:
/quote_bids/want_to_contact_provider/{quote_bid_id}.json不帶 message 的成功路徑:
curl -X POST 'http://localhost:12351/quote_bids/want_to_contact_provider/<quote_bid_id>.json' \
-H 'X-PRO360-Rest-Api-Key: <api-key>' \
-H 'X-PRO360-User-Session-Token: <session-token>' \
-F 'reason=DEFAULT_MESSAGE'帶白名單罐頭 message 的成功路徑:
curl -X POST 'http://localhost:12351/quote_bids/want_to_contact_provider/<quote_bid_id>.json' \
-H 'X-PRO360-Rest-Api-Key: <api-key>' \
-H 'X-PRO360-User-Session-Token: <session-token>' \
-F 'reason=DEFAULT_MESSAGE' \
-F 'message=有興趣,想了解更多細節'成功 response:
{
"status": "success",
"error": 0
}DB 驗證
SELECT
id,
quote_request_id,
quote_service_id,
is_want_to_contact_provider,
contact_provider_on,
total_site_fee,
is_paid_for_subscription,
reason,
requestor_note
FROM quote_bids
WHERE id = <quote_bid_id>;SELECT id, auto_quote_contact_count
FROM quote_requests
WHERE id = <quote_request_id>;若有帶 message:
SELECT id, quote_bid_id, message, classes, model_version, status, created
FROM quote_bid_classify_contacts
WHERE quote_bid_id = <quote_bid_id>
ORDER BY id DESC
LIMIT 10;若要查失敗原因,先查 application log。新專案 LogHelper 會依 level 寫入 monolog tables:
SELECT id, channel, level, message, context, datetime
FROM monolog_YYYYMM
WHERE channel IN ('processWantToContactProvider', 'logQuoteBidTxn')
ORDER BY id DESC
LIMIT 50;SELECT id, channel, level, message, context, datetime
FROM monolog_YYYYMM
WHERE channel LIKE 'quote_bids/want_to_contact_provider/%'
OR channel IN ('processWantToContactProvider', 'logQuoteBidTxn', 'want_to_contact_provider__check_line_id')
ORDER BY id DESC
LIMIT 50;YYYYMM 要換成實測月份,例如 2026-05 查 monolog_202605。staging 實測時也可用 request path 或 quote_bid_id 在 context 裡反查 RequestHttp log。
初步判斷
目前 action、直接 PHPUnit、staging 成功路徑、rejected / insignificant 路徑與 prime / TapPay pay-by-prime 直接測試都已驗完。
已驗項目:
- 成功聯絡 provider。
message被分類為rejected或insignificant時回{"error":17,"status":""}。- 預約時段不符合時轉 new leads 並回 error 18。