QuoteBids Switch To Manual Quote

對應 legacy / web-app 實際 API:

POST /quote_bids/switch_to_manual_quote/{quote_bid_id}.json

quote_bid_idquote_bids.id。body 使用 FormData;只有需要 TapPay 時才帶 prime,不會用 JSON body 傳 quote_bid_id

說明:legacy controller method 是 QuoteBidsController::switch_to_manual_quote($quote_bid_id),web-app 也以 path 傳入 RequestObject.QuoteBid.id。新版 routing 目前保留不帶 path id、改從 body 讀 quote_bid_id 的 fallback,但這不是 web-app 的正式呼叫格式。

功能說明

provider 在尚未聯絡 consumer 的 auto quote bid 上,選擇改成自己手動送出報價時會呼叫此 API。

成功時,這支 API 會把同一筆 quote_bids.id 從 auto quote 未聯絡狀態改成「一般手動報價已送出且已付款」狀態:

  • quote_bids.is_auto_quote 改為 0
  • quote_bids.is_want_to_contact_provideris_paid_for_subscription 改為 1
  • quote_bids.is_contact_charge 改為 0
  • 依手動報價規則重新計算 total_site_feeauto_quote_fee_reason
  • 若原 auto quote fee 曾佔用 quote service category balance,且仍在可還原期間,會先還原 balance。
  • 依付款結果扣 provider wallet,或建立 auto quote debt / card charge / TapPay prime 綁卡與扣款紀錄。
  • 寫入 transactionsquote_user_subscription_logs,並把 subscription log id 回寫到 bid。
  • 建立 SwitchToManualQuote activity,送出 consumer 收到報價 notification,並寫入 request counter 相關 queue。

若 provider 資格、bid 狀態、request 狀態、電話驗證、類別限制或付款檢查失敗,應回 legacy 對應 error,且不應把 bid 轉成已送出手動報價。

快速結論

  • actor:provider session。
  • new API:已補 Endpoint/V1/QuoteBids.php::switch_to_manual_quote()
  • routing:已補 Mapping.php,支援 legacy / web-app 實際呼叫的 /quote_bids/switch_to_manual_quote/{quote_bid_id}.json;若 path 沒帶 id,action 也保留 body quote_bid_id fallback。
  • request body:FormData,只在有 TapPay prime 時帶 prime
  • legacy controller:QuoteBidsController::switch_to_manual_quote()
  • legacy domain:QuoteBid::processSwitchToManualQuote()
  • 正式機流量:2026-05-27 查詢 ip-10-5-2-242 access log,總計 934 筆。
  • 優先度:高。這支有正式流量,且牽涉付款、報價、activity、notification 與 queue。

正式機 Access Log

2026-05-27 查詢機器:ip-10-5-2-242

查詢條件:

LOG_DIR=/var/log/apache2
ACCESS_LOG=get-lancer_access.log
PATTERN='quote_bids/switch_to_manual_quote'

結果:

範圍筆數
get-lancer_access.log + get-lancer_access.log.188
get-lancer_access.log.*.gz846
total934

待補確認:

  • path 分布。legacy method 有 $quote_bid_id 參數,web-app 已確認使用 /quote_bids/switch_to_manual_quote/{quote_bid_id}.json;可再用 access log 確認正式機是否還有非 web-app caller 使用其他 path。

查 path 分布建議:

{
  sudo grep -Eho 'quote_bids/switch_to_manual_quote[^ ?"]*' "$LOG_DIR/$ACCESS_LOG" "$LOG_DIR/$ACCESS_LOG.1"
  sudo find "$LOG_DIR" -maxdepth 1 -type f -name "$ACCESS_LOG.*.gz" -print0 \
    | xargs -0 sudo zgrep -Eho 'quote_bids/switch_to_manual_quote[^ ?"]*'
} | sort | uniq -c | sort -nr

Legacy Controller Rule

legacy 檔案:

/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php:1777-1828

QuoteBidsController::switch_to_manual_quote($quote_bid_id)

  1. 透過 .json route / RequestHandler 進入 API response 流程,且需要 valid API session;request body 實際可為 FormData,web-app 不送 JSON body。
  2. 從 session 取得 provider user_id
  3. 從 request data 取得 prime
  4. 若 Redis key user_credit_card_just_use_{user_id} 存在,且 TTL 在 30 秒內,會 sleep 等待綁卡完成。
  5. 透過 QuoteBid::handleLockAndRetry() 包住 QuoteBid::processSwitchToManualQuote($user_id, $quote_bid_id, $prime)
  6. 成功 response:
{"status":"success","error":0}
  1. ProException response 會帶:
    • translated status
    • error
    • exception extends
    • bank_result_code
    • bank_result_msg

Legacy Domain Rule

legacy 檔案:

/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.php:2668-3033

主要 method:

  • QuoteBid::basicAutoQuoteChecking()2668-2774
  • QuoteBid::processSwitchToManualQuote($user_id, $quote_bid_id, $prime = null)2781-3033

Guard

  1. 取得 provider payoff lock:RedisUtil::acquirePayoffLock($user_id)
  2. basicAutoQuoteChecking($user_id, $quote_bid_id, true, true)
    • bid 必須存在,且不是 removed。
    • actor 必須是 provider。
    • provider / requestor / service 必須 active。
    • bid 必須是 auto quote。
    • bid 尚未聯絡 provider。
  3. provider 必須已電話驗證,否則 error 16。
  4. provider 不能被 BlockedUserConfig 限制或 disable match,否則 error 690。
  5. QuoteCategoryLimitation::isPass() 必須通過,否則 error 690。
  6. lock quote_requests,檢查 quote_bid_count < bid_count_limit,否則 error 15。
  7. request 不能 archived / closed / hired / expired,否則 error 3。

Payment / Bid Update

  1. 把本次計費視為手動報價:quote_bid['QuoteBid']['is_auto_quote'] = 0 後重新跑 getSiteFee()
  2. 若仍在 auto quote restore balance period,呼叫 restoreBalance() 還原 quote service category balance。
  3. 呼叫 processCharge($quote_bid, $site_fee, 0, !empty($prime))
  4. 若有 coupon discount,呼叫 QuoteCoupon::fulfillCoupon()
  5. 更新 quote_bids
    • is_want_to_contact_provider = 1
    • is_auto_quote = 0
    • is_contact_charge = 0
    • is_paid_for_subscription = 1
    • quote_sent_on = now
    • quote_last_update_on = now
    • total_site_fee = site_fee
    • auto_quote_fee_reason = json(site_fee_reason)
  6. 建立 SwitchToManualQuote activity。
  7. 呼叫 chargeByCreditCard($provider_user_id, $wait_for_charge_set, $prime)
  8. commit 後呼叫 completeChargeByCreditCard()

Retry / Failure

legacy 會 retry 最多 10 次。若付款相關錯誤為:

  • AutoRechargeFailed
  • CreditNotEnough
  • RetryCreditCard
  • CreditCardCoolDown

其中 RetryCreditCard 會 invalid 目前 auto quote card,若還有下一張 valid card 則 retry;若沒有可用卡,送 charge fail provider,最後回:

error = 4
status = err_switch_to_manual_quote_4
message = 請先設定信用卡以進行報價

Post Commit Side Effect

成功付款主線後:

  1. sendReceiveQuoteNotification($quote_bid),失敗不阻斷主流程。
  2. 若是 Life55688 request,呼叫 Life55688Util::addEventExpertQuoting()
  3. QuoteCategoriesQuoteService::updateLastQuoteSentOn()
  4. task_event_queue.request_sent_count
  5. task_event_queue.request_requestor_read_count

New Project 搬移注意

  • 不能只更新 quote_bids.is_auto_quote。這支的核心是付款 + 手動報價 side effect。
  • prime 必須一路傳到付款 helper,代表 TapPay 一次性付款 token,不是 boolean。
  • response 要保留 bank_result_code / bank_result_msg
  • 可優先沿用已為 want_to_contact_provider / change_status_rating 補齊的付款 helper,避免另寫一套 payment 行為。
  • queue 驗證要接受 queue 或 log 任一存在。

New PHP 8.2

new 檔案行號對應 legacy職責
Lib/Common/RouterRule/Mapping.php:149-151access log path/quote_bids/switch_to_manual_quote/{quote_bid_id}.json route 到 QuoteBids::switch_to_manual_quote();也支援 path 不帶 id 時由 body 補。
Endpoint/V1/QuoteBids.php:3329-3416QuoteBidsController.php:1777-1828, AppModel.php:1081-1105session guard、quote_bid_id / prime 解析、Redis card bind wait、domain call、lock wait / deadlock retry、success/error response mapping。
Lib/Model/QuoteBid.php:2045-2170QuoteBid.php:2781-3033payoff lock、transaction、fee、payment、bid update、activity、post-commit side effect。
Lib/Model/QuoteBid.php:2217-2366QuoteBid.php:2668-2774, 2810-2885row lock 與 legacy guard 對齊;包含 quote_status_id != Removed
Lib/Model/QuoteBid.php:2396-2430QuoteBid.php:2892-2900auto quote restore balance。
Lib/Model/QuoteBid.php:2432-2449QuoteBid.php:2931-2958payment exception 轉成 err_switch_to_manual_quote_4
Lib/Model/QuoteBid.php:2816-2985, 1616-1659QuoteBid.php:2902, 2925-2971switch 專用付款 helper;處理 wallet、auto refill package、transaction、subscription log、prime 純綁卡 / pay-by-prime、default card debt charge、invalid card retry。
Lib/Model/QuoteCategoriesQuoteService.php:18-85QuoteCategoriesQuoteService.php:271-318成功後更新 service-category last quote time;total_site_fee >= 500 時同步更新 data mart high value quote 欄位。
tests/QuoteBidsSwitchToManualQuoteTest.php:129-304success / already contacted / removed guard / prime bind only覆蓋 wallet 成功、已聯絡 guard、removed bid guard、prime 未達 debt threshold 時只綁卡並保留 unpaid debt。

逐段行數對照

controller / routing:

Legacy 行號New 行號對照內容狀態
access log pathMapping.php:149-151route /quote_bids/switch_to_manual_quote/{quote_bid_id}.json 到 new action。已對齊
QuoteBidsController.php:1777-1784Endpoint/V1/QuoteBids.php:3329-3341action 入口、JSON/API session guard、取得 provider user。已對齊
QuoteBidsController.php:1793Endpoint/V1/QuoteBids.php:3343-3354, 3364-3368取得 quote_bid_idprime。new 同時支援 path id 與 body id。已對齊
QuoteBidsController.php:1795-1803Endpoint/V1/QuoteBids.php:3355-3361Redis user_credit_card_just_use_{user_id} TTL 30 秒內等待綁卡完成。已對齊
QuoteBidsController.php:1805-1814, AppModel.php:1081-1105Endpoint/V1/QuoteBids.php:3363-3380, 3411-3416呼叫 domain method;遇到 lock wait / deadlock 最多 retry 3 次;成功回 {"status":"success","error":0}已對齊
QuoteBidsController.php:1815-1827Endpoint/V1/QuoteBids.php:3374-3399ProException response 保留 error/status/context,並帶 bank_result_code / bank_result_msg已對齊

domain 主流程:

Legacy 行號New 行號對照內容狀態
QuoteBid.php:2791-2798Lib/Model/QuoteBid.php:2047-2053, 2167-2169provider payoff lock acquire / release。已對齊
QuoteBid.php:2803-2808Lib/Model/QuoteBid.php:2060-2065, 2216-2255開 transaction,鎖定 bid row,讀 bid/request/provider/requestor/service/category data。已對齊
QuoteBid.php:2668-2774Lib/Model/QuoteBid.php:2262-2315basicAutoQuoteChecking:bid 存在、quote_status_id != Removed、provider owner、active user/service、is_interested、auto quote、尚未聯絡。已對齊;2026-05-27 補 removed guard regression
QuoteBid.php:2810-2842Lib/Model/QuoteBid.php:2317-2347provider phone confirmed、blocked user、disable match、category limitation。已對齊
QuoteBid.php:2844-2854Lib/Model/QuoteBid.php:2349-2354request 報價人數上限。已對齊
QuoteBid.php:2866-2885Lib/Model/QuoteBid.php:2356-2365, 2372-2391request archived / closed / hired / expired guard,並建立 fee helper 需要的 request shape。已對齊
QuoteBid.php:2888-2890Lib/Model/QuoteBid.php:2067-2078將本次計費視為 manual quote,重新計算 site fee / fee reason。已對齊
QuoteBid.php:2892-2900Lib/Model/QuoteBid.php:2080, 2396-2430auto quote restore balance period 內還原 balance,並寫 restore activity。已對齊
QuoteBid.php:2902, 2925-2927, 2971Lib/Model/QuoteBid.php:2082-2094, 2816-2985, 1616-1659付款主線:wallet / auto refill package / transaction / subscription log / card / prime 純綁卡或 pay-by-prime / default card debt charge。已對齊 legacy threshold;外部金流成功與失敗仍需 staging 實測
QuoteBid.php:2904-2910Lib/Model/QuoteBid.php:2096coupon fulfill。已對齊
QuoteBid.php:2912-2920Lib/Model/QuoteBid.php:2098-2108更新 bid flags、quote time、total site fee、fee reason。已對齊
QuoteBid.php:2922-2923Lib/Model/QuoteBid.php:2110-2118建立 SwitchToManualQuote activity。已對齊
QuoteBid.php:2931-2958Lib/Model/QuoteBid.php:2089-2090, 2432-2449, 1616-1659付款錯誤轉 error=4, status=err_switch_to_manual_quote_4;default card debt charge 會 retry invalid card。已搬移;多卡實際刷卡 fallback 仍需 staging 實測
QuoteBid.php:2973-2975Lib/Model/QuoteBid.php:2133-2140sendReceiveQuoteNotification 失敗不阻斷主流程。已對齊
QuoteBid.php:2977-2997Lib/Model/QuoteBid.php:2142Life55688 expert quoting event。已對齊,實作共用既有 helper
QuoteBid.php:2999-3007, QuoteCategoriesQuoteService.php:271-318Lib/Model/QuoteCategoriesQuoteService.php:18-85, Lib/Model/QuoteBid.php:2144-2150更新 quote_categories_quote_services.last_quote_sent_on;高價 quote 同步更新 data mart。已對齊
QuoteBid.php:3009-3027Lib/Model/QuoteBid.php:2152-2166寫入 request_sent_countrequest_requestor_read_count task event,失敗只記 log。已對齊

目前文件判斷:

  • 已有逐段行數對照,之後 review 應直接用本節檢查,不只看上面的流程摘要。
  • 2026-05-27 已修正:removed bid guard、lock retry、switch payment 改走完整 charge path、high value data mart update。
  • prime 未達 debt threshold 的 bind-only path、達 threshold 的 pay-by-prime path、foreign card rollback、wallet 不足且無卡 error 4 已補 PHPUnit;多卡 invalid fallback 仍需 staging 或可控 fake fixture 實測。

Staging 實測

2026-05-28:web-app 舊呼叫格式

呼叫:

POST https://api-staging.pro360.com.tw/quote_bids/switch_to_manual_quote/2147689774.json
Content-Type: multipart/form-data
body: empty FormData

response:

{"status":"success","error":0}

DB 連線:

{"db_name":"getlancer","db_host":"ip-172-23-2-24","db_port":"3306"}

主狀態:

quote_bids.id = 2147689774
quote_request_id = 22206
provider_user_id = 8617
quote_status_id = 2
provider_status_id = 2
is_auto_quote = 0
is_want_to_contact_provider = 1
is_paid_for_subscription = 1
is_contact_charge = 0
is_restore_balance = 1
total_site_fee = 160
quote_user_subscription_log_id = 20799
quote_sent_on = 2026-05-28 03:40:55
quote_last_update_on = 2026-05-28 03:40:55
auto_quote_fee_reason = {"8":{"reason":"Normal","discount":1}}

付款 / subscription log:

transactions.id = 36840
transaction_type_id = 40
amount = 160
real_pay_date = 2026-05-28 03:40:56
stripe_charge_id = D20260528W1HImO
 
quote_bids.quote_user_subscription_log_id = 20799
quote_user_subscription_logs.id = 20799
amount = 160
is_paid = 1

同 provider 原本已有一筆 unpaid auto quote debt:

transactions.id = 36839
amount = 176
real_pay_date = 2026-05-28 03:40:56
stripe_charge_id = D20260528W1HImO

這表示 empty FormData / 無 prime 的舊呼叫走了「已有有效自動報價卡片,刷 auto quote debt」路徑;本次 160 與既有 176 一起完成付款,符合 legacy processCharge() + chargeByCreditCard() 的行為。

side effect:

quote_activities.quote_activity_type_id = 52  Auto Quote Restore Balance
quote_activities.quote_activity_type_id = 54  Switch to Manual Quote
event_queue_log_4.event_key = Receive_Quote
task_event_queue_log.event_key = request_sent_count
task_event_queue_log.event_key = request_requestor_read_count

注意:quote_user_subscription_logs.quote_bid_id 是 signed int,遇到 2147689774 這種大 id 會寫成 2147483647 上限值;驗證 subscription log 必須用 quote_bids.quote_user_subscription_log_id -> quote_user_subscription_logs.id,不要用 quote_user_subscription_logs.quote_bid_id 當唯一反查依據。

Verification

測試指令:

docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/QuoteBidsSwitchToManualQuoteTest.php

2026-05-28 結果:

OK (7 tests, 75 assertions)

覆蓋:

  • provider 可透過 /quote_bids/switch_to_manual_quote/{quote_bid_id}.json 將 auto quote 轉 manual quote。
  • wallet 足額時扣 wallet、寫 transactions.class = QuoteBid / transaction_type_id = 33、建立 subscription log。
  • 更新 quote_bids.is_auto_quote = 0is_want_to_contact_provider = 1is_paid_for_subscription = 1quote_sent_onquote_last_update_ontotal_site_fee
  • 建立 SwitchToManualQuote activity。
  • 寫入 request_sent_count / request_requestor_read_count task event。
  • 已聯絡 bid 保留 legacy guard:error = 0, status = err_switch_to_manual_quote_6
  • removed bid 保留 legacy not-found guard:error = 1, status = err_switch_to_manual_quote_1,不寫 transaction / subscription log。
  • prime 且 debt 未達 threshold 時只 bind card,transaction 保持 unpaid,對齊 legacy chargeByCreditCard() amount = 0 行為。
  • prime 且 debt 達 threshold 時走 TapPay pay-by-prime,transaction 會補 real_pay_date / stripe_charge_id
  • prime foreign card failure 回 error = 211, status = err_change_status_rating_211,transaction / subscription log / bid update rollback。
  • wallet 不足、無有效 auto quote card、無 prime 時回 error = 4, status = err_switch_to_manual_quote_4,不寫 transaction。

額外驗證:

  • tests/QuoteBidsChangeStatusRatingTest.php 已補 fixture debt 隔離與測試期間有效 auto quote card 準備:tearDown 後還原,避免本機 baseline debt / card 狀態影響 payment guard。
  • tests/QuoteBidsChangeStatusRatingTest.php 重跑結果:OK (15 tests, 144 assertions)

建議測試矩陣

  • provider wallet 足夠,無 prime,成功。
  • wallet 不足,無 prime,回 error 4。
  • wallet 不足,有 prime,TapPay pay-by-prime 成功。
  • prime 但 TapPay foreign card / charge fail,確認 response mapping 與 rollback。
  • provider 未電話驗證,回 error 16。
  • request closed / expired / hired,回 error 3。
  • bid 已聯絡,回 existing contact guard。