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_activity option、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 的正式洽談狀態。

  1. Consumer 在某個 quote_bid 上按「聯絡專家」。
  2. API 用 session 解析 consumer user_id
  3. 檢查該 consumer 是否為 quote_bids.quote_request_id -> quote_requests.user_id 的 request owner。
  4. 若是 auto quote 且尚未聯絡,先檢查 provider 服務時段是否仍可配合。
  5. 若 request 有帶 message,先寫入對話,並做 message 分類。
  6. 若 message 被判定為 rejectedinsignificant,回 {"error":17,"status":""},不進入聯絡扣款主線。
  7. 通過後呼叫 QuoteBid::processWantToContactProvider()
    • 更新 quote_bids.is_want_to_contact_provider = 1
    • 記錄 contact_provider_on
    • 處理 site fee / subscription / transaction
    • 寫 quote activity
    • 送後續 queue / event
  8. 成功後補 quote_conversation_with_requestor_note()
  9. 回傳 {"status":"success","error":0}

資料關聯核心:

規則資料表欄位
確認 consumer 是案件 ownerquote_requestsid, user_id
找聯絡目標 providerquote_bidsid, quote_request_id, provider_user_id, quote_service_id
開啟聯絡狀態quote_bidsis_want_to_contact_provider, contact_provider_on
統計 auto quote 聯絡數quote_requestsauto_quote_contact_count
message 分類紀錄quote_bid_classify_contactsquote_bid_id, message, classes

Legacy 主流程

  1. 驗 JSON request 與 API session。
  2. 從 session 解析 requestor user_id
  3. 讀取 input:
    • reason
    • preserve_note
    • prime
    • message
  4. 確認 auto quote bid 是否可聯繫。
  5. 若有 message,先做訊息檢查、分類與 quote_conversation()
  6. 呼叫 QuoteBid::processWantToContactProvider(...)
  7. 成功後補 quote_conversation_with_requestor_note()
  8. 回傳:
{
  "status": "success",
  "error": 0
}

新專案現況

新專案已補同 action endpoint,並串接既有核心 contact provider 主線:

Lib/Model/QuoteBid.php::processWantToContactProvider()

已被以下流程重用:

  • Lib/SearchAdd/AuthorizedContactProcessor.php
  • Lib/SearchAdd/DirectReserveProcessor.php
  • Lib/SearchAdd/DirectUserProcessor.php

Routing 未另外 mapping 這條帶 id 的舊路徑,會走 Origin rule:

  • Lib/Common/RouterRule/Mapping.php 沒有 quote_bids/want_to_contact_provider/{quote_bid_id}.json 的 explicit mapping
  • Endpoint/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、reasonpreserve_noteprimemessage,初始化 model / reject flag。
1847-1868讀 bid/request;若 auto quote 尚未聯絡,檢查服務時段;不符合時轉 new leads 並回 error=18
1871-1927message whitelist / line id / phone / link guard;必要時呼叫 QuoteBidClassifyContactrejected / insignificant 設為 rejected。
1933-1984有 message 時先寫 quote_conversation(),成功後送 QuoteServiceCategory::sendAutoMessage(),deadlock / timeout 最多 retry 5 次。
1986-1988rejected message 回 error=17,status 為空字串。
1990-2009呼叫 processWantToContactProvider(),deadlock / timeout 最多 retry 5 次。
2010-2021成功後補 quote_conversation_with_requestor_note(),response {"status":"success","error":0}
2022-2025ProException 走 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-4056transaction retry;narrow match 情境先轉 auto quote,接著做 basicAutoQuoteChecking()
4059-4103blocked / 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=1contact_provider_onis_paid_for_subscription=1is_interested=1reasonrequestor_note
4224-4245建立 AutoQuoteContact activity;非 narrow match 時 param1=NULLparam2=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-4363processWantToContactProvider 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 Provider701 (no message)Auto Quote Want to Contact Provider
2311-2326task_event_queue.sent_contact_provider_sms,建立 email task。
2339-2344建立 EventParameterDO,先放 email task。
2346-2379reason=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-26871830-1835入口 want_to_contact_provider();驗 session,取 quote_bid_id;id invalid 回 legacy error。
2689-26981837-1845reasonpreserve_noteprimemessage,初始化 model / reject flag。
2700-27351847-1868讀 bid/request;auto quote 尚未聯絡時檢查服務時段;不符合時呼叫 legacy 對等 fallback 並回 error=18
2737-27861871-1927message whitelist / line id / phone / link guard;必要時呼叫 QuoteBidClassifyContact,判斷 rejected。
2788-28371933-1984有 message 時寫 quote_conversation(),成功後送 auto message,deadlock / timeout 最多 retry 5 次。
2839-28441986-1988rejected message 回 {"error":17,"status":""},避免落到 generic exception response。
2846-28701990-2009呼叫 processWantToContactProvider(),deadlock / timeout 最多 retry 5 次。
2872-28832010-2021成功後補 quote_conversation_with_requestor_note(),response {"status":"success","error":0}
2884-28942022-2025PRO360Exception 走 legacy-style response:{"error":code,"status":message_key};無 explicit message key 時保留 exception message。

domain method:

Lib/Model/QuoteBid.php::processWantToContactProvider()
new 行號對應 legacy職責
683-7084009-4016入口並讀 options;PHP 8 use_payoff_lock 預設 true,search_add 三個 caller 傳 false。current Legacy 沒有此 lock。
710-7314031-4056開 transaction;narrow match 情境先轉 auto quote。
731-745, 902-9514056-4134basicAutoQuoteChecking()、讀 request、blocked / disabled / blocked chat guard與 error mapping。
751-8164144-4191決定 site fee,處理 subscription / wallet / credit card / transaction、coupon 與 free trial。
778-7944193-4221更新 bid:is_want_to_contact_provider=1contact_provider_onis_paid_for_subscription=1is_interested=1reasonrequestor_note
796-8054193-4221非已聯絡且非 narrow 時累加 quote_requests.auto_quote_contact_count
833-8574224-4245narrow accept activity 與 AutoQuoteContact activity;非 narrow match 時 AutoQuoteContact.param1=NULLparam2=reason;narrow match 時 NarrowProviderAccept.param1=reasonAutoQuoteContact.param1=WantProviderQuote.param2narrow_matchparam2=NULL
861-8834253-4264, 4344-4351bid_contact_count、my work unread count;只有非 narrow match且非 direct reserve 時,才寫 sent_contact_provider_smsContact_Pro event。
892-8994318-4342contact charge 完成後,若 request 有 quote_request_life_tokens,送 Life55688 EVENT_EXPERT_QUOTING;一般非外層 transaction 呼叫時,先 commit 再送外部 side effect。
885-8994266-4268, 4318-4342commit transaction、執行 transaction 後 side effect,成功回 true。
902-9574270-4315charge failure code 轉 narrow / new leads,記 processWantToContactProvider log,對外回 error=16
958-9704344-4363PRO360Exception 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-51DirectReserveProcessor.php:116-129DirectUserProcessor.php:123-135use_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 guard 1 test / 9 assertions;本 detail 的完整 contact regression 12 tests / 184 assertions

date mismatch fallback:

Lib/Model/QuoteBid.php::switchWantToContactProviderToNarrowMatch()
new 行號對應 legacy職責
4080-40974509-4526basicAutoQuoteChecking() 讀並鎖定同一筆 auto quote bid;決定 param2=auto_quote/contact_chargeis_related
4099-41174528-4540保留同一筆 bid,改為 Send/Send、清 quote_sent_onis_auto_quote=0is_want_to_contact_provider=1is_narrow_match=1narrow_status_id=WAITING
4119-41284543建立 WantProviderQuote activity,receiver_user_id=requestor_user_idparam1=NULLparam2=auto_quote
4130-41444545-4553commit / rollback;失敗時回 legacy error=16status=err_want_to_contact_provider_16
4147-41584558-4562呼叫 sendConsumerWantProviderQuoteMail();新專案為避免通知外部錯誤影響 API response,通知例外只記 log。

notification payload:

Lib/Model/QuoteBid.php::buildContactProviderEventTask()
new 行號對應 legacy職責
979-10142271-2292讀 provider/requestor/request title,建立 dashboard / chat URL、security hash、dynamic short URL。
1015-10352294-2309, 2322-2326建立 email task;template 為 701 Auto Quote Want to Contact Provider701 (no message)Auto Quote Want to Contact Provider
1036-10402339-2344建立 EventTaskDO,top-level 保持 legacy 形狀:user_idquote_service_idtasks;不輸出 quote_request_id / quote_bid_id
1042-10682346-2379reason=GET_PHONE_NUMBER_OF_VENDOER 時補 push / web_push,target 指向 quote bid。
1071, 829-8352381-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_countquote_user_subscription_logs、wallet 足額路徑的 transaction_type_id=33AutoQuoteContact activity、bid_contact_countsent_contact_provider_smsContact_Pro email / push event,並鎖定 event top-level 不帶 quote_request_id / quote_bid_id
tests/QuoteBidsWantToContactProviderTest.phpcontact 前若已存在 unrelated transaction_type_id=40 auto quote submission,仍不可跳過本次 contact charge / subscription log;測試固定 wallet 足額,因此本次 legacy 分支應新增 transaction_type_id=33quote_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.phpis_contact_charge=1 時先使用 quote_bid_site_fees.fee_type=contact_charge 的 snapshot fee,再建立 subscription log / transaction。
tests/QuoteBidsWantToContactProviderTest.phpNewAutoQuoteTrial fee reason 成立時,成功 contact 後扣 users.free_auto_quote,並以 legacy discount 後金額寫入 bid / subscription log。
tests/QuoteBidsWantToContactProviderTest.phpwallet 不足、無 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.phpwallet 不足、無 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.phpmessage 分類為 rejected 時回 {"error":17,"status":""};驗只寫 message / classify row,不產生 contact、payment、activity、Contact_Pro side effect。
tests/QuoteBidsWantToContactProviderTest.phpdate match 失敗時回 {"error":18,"status":"err_want_to_contact_provider_18"},並走 switchWantToContactProviderToNarrowMatch();驗同 bid 轉 narrow/new lead、quote_sent_on=NULLis_related=ErrorReserveAutoQuoteWantProviderQuote.param2=auto_quote、不扣款、不寫 Contact_Pro

已存在的間接 coverage:

test 行號覆蓋內容
tests/SearchAddStep7IntegrationTest.php:553-640authorized search add 會呼叫 processWantToContactProvider();驗 contact_provider_onis_paid_for_subscription、subscription log、auto_quote_contact_countAutoQuoteContact activity、sent_contact_provider_sms、transaction、Contact_Pro email task。
tests/SearchAddStep7IntegrationTest.php:743-780reason=GET_PHONE_NUMBER_OF_VENDOER 時,驗 Contact_Pro 會帶 push / web_push tasks。
tests/QuoteBidsProviderAcceptNarrowMatchTest.php:234-386narrow accept path 會重用 contact provider 主線,驗 is_want_to_contact_provider、transaction、NarrowProviderAccept.param1=reasonAutoQuoteContact.param1=narrow_matchAutoQuoteContact.param2=NULL,並驗證不送一般 sent_contact_provider_sms / Contact_Pro
tests/QuoteBidsProviderAcceptNarrowMatchTest.phpnarrow accept coupon path 會套用 Narrow_Match_Use_Coupon,成功後 quote_coupons.quote_bid_id 指向本次 bid、is_fulfill=1fulfill_on 已寫入,subscription amount 使用折扣後 fee。
tests/SearchAddStep7IntegrationTest.php:316-468direct reserve 成功 contact 會重用 contact provider 主線,驗 reserve / pause / message / Direct_Reserve_Pro,並驗證不送一般 sent_contact_provider_sms / Contact_Pro
tests/SearchAddStep7IntegrationTest.phpdirect 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-1424direct user 命中後會重用 contact provider 主線;低 weekly balance 情境仍收 full direct user fee,且不修改 quote_service_categories.balance,對應 legacy is_check_budget=false 語意。
tests/SearchAddStep7IntegrationTest.phpauthorized contact 遇 RetryCreditCard 且只有一張 card 時,仍回 error=16、fallback new leads、停用 auto quote card 並送 Auto_Charge_Fail
tests/SearchAddStep7IntegrationTest.phpauthorized 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_reserveis_narrow_match 通知邊界:legacy 只有非 narrow、非 direct reserve 才呼叫 sendContactProvider()。新版已改為只在相同條件下寫 sent_contact_provider_smsContact_Pro
  • basicAutoQuoteChecking() requester active guard:新版曾以 provider_user_id 查 requester user,已改回 quote_requests.user_id
  • basicAutoQuoteChecking() service active / archived guard:legacy processWantToContactProvider()$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 剩餘額並寫 UnderCharge reason;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 時不建立 AutoQuoteContact activity。新版已補相同 option;目前 repo 內沒有 caller 傳 false,但共用方法保留此邊界。
  • narrow activity 參數:legacy 先建立 NarrowProviderAccept.param1=reason,接著把 $reason 清成 null;AutoQuoteContact.param1 取來源 WantProviderQuote.param2,取不到時用 narrow_matchparam2=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_charge snapshot、coupon fulfill、NewAutoQuoteTrialusers.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_Pro worker 發送端可能因 staging notification template / SMS provider 設定回 notification template setting not foundsend error,不列為 API 主流程阻塞。

Payment Parity Follow-up Matrix

payment parity 以測試先行,不先改付款主線。處理狀態:

狀態情境legacy 規則驗證點
已補contact charge fee snapshotis_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 fulfillsite_fee_reason[Coupon] 且非 contact charge 時,成功 contact 後呼叫 QuoteCoupon::fulfillCoupon()quote_coupons.quote_bid_id 指向本次 bid,is_fulfill=1fulfill_on 已寫入,subscription amount 使用折扣後 fee
已補new auto quote trialsite_fee_reason[NewAutoQuoteTrial] 時,成功 contact 後 users.free_auto_quote = free_auto_quote - 1provider free_auto_quote 減 1,total_site_fee 使用 legacy FreeAutoQuote discount 後金額,subscription amount 對齊
已補existing total_site_fee > 0legacy 無 subscription log 時仍重新跑 getSiteFee(),不是直接沿用 bid 上既有 fee建立 bid total_site_fee 與 request fee 不一致的 fixture,確認本次 contact 以重新計算結果為準
已補direct user full fee boundarydirect user 由 caller 建 bid 決定 full fee,並傳 is_check_budget=false;不得被 weekly budget / free trial 改寫SearchAddStep7IntegrationTest::testCreatesDirectUserMatchquote_bids.total_site_fee = quote_requests.auto_fee = direct_fee
已補retry next cardRetryCreditCard 後 invalid 目前卡,若還有下一張 auto quote card,while loop 重試SearchAddStep7IntegrationTest::testAuthorizedModeRetriesNextCardWhenRetryCreditCardFails 驗第一張卡 invalid、第二張卡成功扣款、transaction paid、未送 charge fail
已補prime / pay-by-prime direct testlegacy 帶 prime 時會走 TapPay pay-by-prime;foreign card 失敗時不落 contact side effectQuoteBidsWantToContactProviderTest::testConsumerCanContactProviderAndPayAutoQuoteDebtByPrimeWhenWalletInsufficient 驗 transaction type 40 paid + TapPay charge id;testConsumerContactProviderPrimeForeignCardFailureHasNoContactSideEffectserror=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_bidsis_auto_quote=1is_narrow_match=0narrow_status_id=0is_want_to_contact_provider=1is_paid_for_subscription=1contact_provider_on=2026-05-19 08:01:08quote_user_subscription_log_id=20573reason=DEFAULT_MESSAGEtotal_site_fee=111
quote_requestsauto_quote_contact_count=5;request 未 archived / closed / expired / hired。
transactions最新 transaction id=36449transaction_type_id=33amount=111.00real_pay_date=2026-05-19 08:01:08
quote_activitiesAutoQuoteContact(50)provider_user_id=7331requestor_user_id=8617receiver_user_id=8617param1=NULLparam2=DEFAULT_MESSAGE
messages寫入 sender / receiver 兩列,內容為 有興趣,想了解更多細節
quote_bid_classify_contacts無資料;原因是該 message 存在於 chat_can_messagesis_reject=0,不會進分類。
task_event_queue_logbid_contact_countsent_contact_provider_sms 成功進 log,status_id=1
event_queue_log_4Contact_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實測時看到的問題
2147686292contact 前已有 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 當下建立:

tablelegacy side effect
transactions新增 transaction_type_id=33amount=total_site_fee,wallet 成功扣款時有 real_pay_date
quote_user_subscription_logs新增一筆 contact subscription log。
quote_bidsquote_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_shapecpaid_cntunpaid_cnt
type40685265420
type333153150

正式機 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=3340,同時建立 quote_user_subscription_logs 並回寫 quote_bids.quote_user_subscription_log_id

已修正的新專案對照:

new codelegacy 對照說明
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 chargecontact charge 會新增對應 transaction;wallet 付款為 transaction_type_id=33,auto quote card / debt 路徑為 transaction_type_id=40,同時建立 quote_user_subscription_logs
QuoteBid::updateUserSubscriptionLogId()legacy bid 回填 subscription logquote_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_bidsquote_request_id=21874provider_user_id=5319quote_service_id=3078is_auto_quote=1is_narrow_match=0narrow_status_id=0is_want_to_contact_provider=1is_paid_for_subscription=1is_interested=1contact_provider_on=2026-05-20 02:22:10quote_user_subscription_log_id=20611reason=DEFAULT_MESSAGEtotal_site_fee=111
quote_requestsid=21874auto_quote_contact_count=1;request 未 archived / closed / expired / hired。
quote_user_subscription_logsid=20611user_id=5319amount=111is_paid=1payment_gateway_id=1real_pay_date=NULLpoint_rate=11quote_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=5319requestor_user_id=8617receiver_user_id=8617param1=NULLparam2=DEFAULT_MESSAGE
messages寫入 sender / receiver 兩列,內容為 有興趣,想了解更多細節
quote_bid_classify_contacts無資料;原因是該 message 存在於 chat_can_messagesis_reject=0,不會進分類。
task_event_queue_logbid_contact_countsent_contact_provider_sms 已進 log,status_id=1
event_queue_log_4Contact_Pro 已進 log,email template 為 701 Auto Quote Want to Contact Provider,payload top-level 維持 legacy 形狀:user_idquote_service_idtasks

本筆與 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=insignificantmodel_version=gpt-5.2-2025-12-11params=[{"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
  • 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.php
  • get-lancer-docker-2/get-lancer/app/Lib/ClassifyModelUtil.php
  • get-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_idmessageclassesmodel_versionstatusparams

資料表與 side effect

主要會影響:

  • quote_bids
    • is_want_to_contact_provider
    • contact_provider_on
    • is_paid_for_subscription
    • total_site_fee
    • reason
    • requestor_note
  • quote_requests
    • auto_quote_contact_count
  • quote_activities
    • AutoQuoteContact
    • legacy 非 narrow match 時 param1 = NULLparam2 = reason
  • transactions / subscription log

後續 queue / event 新舊對照:

tableevent_keylegacy 寫入點新專案寫入點新專案 worker / handler用途
task_event_queuebid_contact_countget-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_queuesent_contact_provider_smsget-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_queueContact_Proget-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.php
  • get-lancer-docker-2/get-lancer/app/Model/EventQueue.php

新專案的 queue consumer 較清楚集中在 Endpoint/V1/TaskEventQueue.phpEndpoint/V1/EventQueue.phpContact_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_logevent_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 被分類為 rejectedinsignificant 時,會先寫入 message / message activity,接著回:

{
  "error": 17,
  "status": ""
}

新專案不能讓這個錯誤落到 Router 的 PRO360Exception handler,否則 response key 會變成 message,與 legacy 不一致。

AutoQuoteContact activity

legacy QuoteBid::processWantToContactProvider() 建立 AutoQuoteContact activity 時:

情境param1param2
非 narrow matchNULLreason
narrow matchnarrow_match 或來源 WantProviderQuote.param2NULL

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:目前 log 381 筆、.1502 筆、gz 有 8353
  • 排除 _for_call_jwt 後:目前 log 372 筆、.1491 筆、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_jwt8740
排除 JWT 後只看 POST6386

87398740 差 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 = processWantToContactProvider
  • channel = logQuoteBidTxn

常見判讀:

  • response error 只代表對外訊息
  • 真正付款、guard、transaction 失敗原因要以 log channel 為準

實測準備

這支會改 DB、可能扣款或產生 transaction,不要隨便拿正式資料直接測。建議先用本機 / 測試 DB,並挑明確可回復的測試 bid。

2026-05-19 接續策略:

  • consumer_want_provider_quote staging 驗證已完成,下一支同步這支 API。
  • 優先驗 response、quote_bidsquote_requests.auto_quote_contact_countquote_activities.AutoQuoteContacttransactions / 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-05monolog_202605。staging 實測時也可用 request path 或 quote_bid_idcontext 裡反查 RequestHttp log。

初步判斷

目前 action、直接 PHPUnit、staging 成功路徑、rejected / insignificant 路徑與 prime / TapPay pay-by-prime 直接測試都已驗完。

已驗項目:

  1. 成功聯絡 provider。
  2. message 被分類為 rejectedinsignificant 時回 {"error":17,"status":""}
  3. 預約時段不符合時轉 new leads 並回 error 18。