QuoteBids Change Status Rating

legacy rating 實際是「provider 送出報價 / 更新報價」,不是 consumer 評分。

快速流程

這支 API 是 provider 在案件聊天室送出報價。讀這份文件時可以先用下面流程理解,再往下看各 guard、付款與 queue 細節。

1. Route
   /quote_bids/change_status/rating/{quote_bid_id}.json
   -> QuoteBids::change_status_rating()
   -> QuoteBid::processChangeStatusRating()
 
2. 前置檢查
   確認操作者是 bid provider、電話已驗證、帳號未被 blocked / fraud / category limitation 擋下。
   再確認 request 未關閉、未過期、未 hired,且 bid 尚未送出。
 
3. 判斷報價型態
   is_contact_charge = true
   -> 保證回覆 / contact charge 報價
 
   is_contact_charge = false
   -> 一般手動報價
 
4. 一般手動報價
   檢查同 provider 是否已用其他 bid 報價、request 報價數是否達上限。
   計算 site fee。
   wallet 足額就立即扣 wallet。
   wallet 不足但有 auto quote card / prime,就先寫 unpaid auto-quote debt,必要時立即刷卡。
   更新 quote_bids 為 InProgress,並增加 quote_requests.quote_bid_count。
 
5. 保證回覆 / contact charge 報價
   檢查 prime 或 provider auto quote card。
   計算 contact charge fee,寫 quote_bid_site_fees。
   更新 quote_bids 為 InProgress、is_auto_quote=1、is_contact_charge=1。
   建立 quote_bid_calls。
 
6. 共用成功 side effect
   更新報價內容、price_note、pricing fields、request unread state。
   建立 SubmitQuote activity。
   寫附件 message。
   更新 quote template / last used template。
   coupon fulfill。
   更新 FETNET 綁定 consumer 的未讀 counter。
   插入 Receive_Quote notification event。
   插入 quote_bid_calc_counter task event。
   插入 new_quote_bid_rating task event。
   provider 第一筆報價時檢查 referral。

對應入口

legacy 主要入口:

/quote_bids/change_status/rating/{quote_bid_id}.json
get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php::change_status($filter, $quote_bid_id)
filter = rating

注意:

  • QuoteBidsController::rating() 是另一個獨立 action,但 legacy 實際主線在 change_status(..., rating, ...)
  • API 路由與遷移/API Migration/pro360_api_82/api_migration/quote_bids.md 內的 /quote_bids/rating.json 不等於本文件的 change_status/rating

New API 現況

目前已搬主要 legacy 流程:

  • Lib/Common/RouterRule/Mapping.php 已新增 /quote_bids/change_status/rating/{id}.json
  • Endpoint/V1/QuoteBids.php::change_status_rating() 已新增
  • QuoteBid::processChangeStatusRating() 已新增
  • tests/QuoteBidsChangeStatusRatingTest.php 已覆蓋一般手動報價成功、wallet 足額扣款、wallet 不足寫 unpaid auto-quote debt、無卡 guard、default card foreign / retry / cooldown、prime bind、prime pay debt、prime foreign card、contact charge 成功、contact charge 無卡 guard、Receive_Quote 通知 queue、quote template / last used template、非 provider、已報價、request closed
  • TapPayUtil 已加 CHANGE_STATUS_RATING_TEST_FAKE_TAPPAY_RESULT 測試 hook,避免 PHPUnit 打 TapPay 外部服務。
  • QuoteBid::sendReceiveQuoteNotification() 已補 legacy Receive_Quote event,包含 MoEngage、email、push、web push、SMS;Life55688 / FETNET 分支也保留。
  • coupon fulfill、quote template 更新、FETNET unread、第一筆報價 referral check 已補。

新專案可重用的部分:

  • PreviewQuoteFeeUtil::getSiteFee():site fee 計算。
  • QuoteBid::hasQuoteUserSubscriptionLog():避免同 bid 重複付款。
  • QuoteBid::processWantToContactProvider() 內有 wallet / card / prime / debt 相關邏輯,但它是 contact provider / narrow match 流程,不等價於 manual quote rating。
  • TaskEventFactory 已有 new_quote_bid_ratingrequest_bid_count_limit handler。

待驗:

  • contact charge core path 已有 PHPUnit;仍需要用 staging case 驗實際 provider/card/prime 資料下的 legacy response。
  • 一般手動報價已覆蓋 fee=0 core path、wallet 足額扣款、wallet 不足但有 auto quote card 時先寫 unpaid debt。
  • default card foreign / retry / cool-down 已有 fake charge PHPUnit;prime 綁卡、prime 付清 debt、prime foreign card 已有 fake TapPay PHPUnit。
  • Receive_Quote queue payload 已有 PHPUnit;實際 worker 發信 / push / SMS 建議用 staging 真實 token 做端到端驗證。

主要輸入

來源欄位用途
pathquote_bid_id目標 bid
form-dataprice_note報價說明,也會被 abnormal keyword 檢查
form-datarating_typeflat / hourly / quote / required
form-dataflat_rate_at單次報價金額
form-datahourly_rate_at / hourly_rate_for時薪與工時
form-dataprimeTapPay prime;用於綁卡或立即處理欠款
form-datais_contact_charge是否走保證回覆 / contact charge 報價
form-datalast_used_template_id更新 provider 最近使用模板
form-dataattachments報價附件,會寫入 message

is_contact_charge 判斷需保留 legacy 相容規則:

  • 如果 request 沒帶 is_contact_charge,沿用 quote_bids.is_contact_charge == 1
  • 如果 request 帶 is_contact_charge=1,還要確認 category 支援 contact charge。
  • 其他狀況視為一般手動報價。

前置 Guard

這些 guard 在分支前先做:

條件legacy error
bid 不存在、不是 provider、基本資料異常error=1, err_change_status_rating_1
quote_bids.is_can_not_quote = 1error=11, err_change_status_rating_3
provider phone 未驗證error=6, err_change_status_rating_6
blocked usererror=7, err_change_status_rating_7
blocked user config / category limitation 擋下error=690, err_change_status_rating_690
fraud tagerror=8, err_change_status_rating_7
auto quote card 已失效error=11, err_change_status_rating_11

另有 abnormal 檢查:

  • AbnormalConfig::checkNewUser(..., change_status_rating)
  • AbnormalConfig::checkListKeyword(..., change_status_rating)
  • 例外只寫 change_status_rating__checkNewUser log,不會直接中止報價。

如果 consumer 端已移除此 bid,legacy 會在未 mute 的情況下嘗試 restore QuoteBidRemove

分支一:保證回覆 / Contact Charge 報價

進入條件:is_contact_charge = true

transaction 內主要規則:

  1. SELECT quote_bids ... FOR UPDATE 鎖 bid。
  2. is_auto_quote != 0narrow_status_id != 0 或已 quote_sent_on,拒絕。
  3. prime 時,先處理 provider auto quote 欠款或綁卡。
  4. 沒有 prime 且 provider 未開 auto quote card,拒絕。
  5. 檢查 request:
    • archived / closed / expired 拒絕。
    • hired_count > 0 拒絕。
    • bid 已送出或已有 subscription log 拒絕。
  6. 計算 site fee,寫 quote_bid_site_fees
  7. 更新 bid 為保證回覆報價。
  8. 建立 quote_bid_calls,若已超出可通話 quota 則標成 out-of-quota。

成功時主要欄位:

table欄位
quote_bidsquote_sent_on = now
quote_bidsquote_status_id = InProgress
quote_bidsprovider_status_id = InProgress
quote_bidsis_interested = 1
quote_bidsis_auto_quote = 1
quote_bidsis_contact_charge = 1
quote_bidsis_want_to_contact_provider = 0
quote_bidstotal_site_fee, auto_quote_fee_reason
quote_bid_site_feestype = contact_charge
quote_bid_calls建立 provider call row

分支二:一般手動報價

進入條件:is_contact_charge = false

transaction 內主要規則:

  1. 若 Redis user_credit_card_just_use_{user_id} TTL 在 1 到 30 秒內,legacy 會 sleep 等待綁卡狀態穩定。
  2. 最多 retry 10 次。
  3. SELECT quote_bids ... FOR UPDATE 鎖 bid。
  4. is_auto_quote != 0narrow_status_id != 0 或已 quote_sent_on,拒絕。
  5. SELECT quote_requests ... FOR UPDATE 鎖 request 名額欄位。
  6. 同 provider 對同 request 已有其他 InProgress / Hired bid,拒絕。
  7. 檢查 request:
    • archived / closed / expired 拒絕。
    • hired_count > 0 拒絕。
  8. 檢查 bid count limit:
    • 一般用 quote_requests.quote_bid_count
    • QuoteRequestLimitId::isLimit(request_id),要加上 auto_quote_contact_countcontact_charge_contact_count
  9. 若尚未有 subscription log,計算 site fee,並依 wallet / card 狀態建立付款紀錄。
  10. 先寫入 quote_sent_on 與 bid 狀態,降低重複送出風險。
  11. 更新 quote_requests.quote_bid_count
  12. 若達 bid limit,插入 task_event_queue.request_bid_count_limit
  13. wallet 足額時立即扣 wallet,寫 transactions.QuotePaymentForSubmissionquote_user_subscription_logs.real_pay_date
  14. wallet 不足但有 auto quote card / prime 時,先寫 transactions.QuotePaymentForSubmissionAutoQuote unpaid debt;只有達 debt 門檻或需立即扣款時才刷卡。

成功時主要欄位:

table欄位
quote_bidsquote_sent_on = now
quote_bidsquote_status_id = InProgress
quote_bidsprovider_status_id = InProgress
quote_bidsis_interested = 1
quote_bidsis_paid_for_subscription = 1
quote_bidsis_contact_charge = 0
quote_bidstotal_site_fee, auto_quote_fee_reason
quote_requestsquote_bid_count 更新
transactions / quote_user_subscription_logswallet 足額為 paid submission;wallet 不足為 unpaid auto-quote debt

報價內容更新

兩個分支成功後,還會共用後段報價內容更新。

固定會更新:

  • quote_bids.quote_last_update_on
  • quote_bids.price_note
  • quote_bids.provider_last_read_on = now + 3 seconds
  • quote_bids.is_requestor_readed = 0
  • quote_bids.is_quote_viewed = 0
  • quote_requests.is_archived = 0
  • quote_requests.is_requestor_readed = 0

rating_type 規則:

rating_type效果
flatflat_rate_atquote_amountpricing_unit=單次
hourlyhourly_rate_atquote_amountpricing_unit=小時;有 hourly_rate_for 時用 rate * hours
quoteis_on_site_visit_required_for_quote=1,關閉 book-it-now
requiredis_more_information_required=1,關閉 book-it-now

每次更新前會重置多個 pricing 欄位,避免殘留舊報價模式:

  • flat_rate_at
  • hourly_rate_at
  • hourly_rate_for
  • per_unit_at
  • per_unit_for
  • units_of
  • quote_amount
  • is_on_site_visit_required_for_quote
  • is_more_information_required

其他 side effect:

  • quote_activities.SubmitQuoteUpdateQuote
  • 附件逐筆寫入 quote conversation message。
  • 更新 users.last_used_template_id,必要時儲存 quote_templates
  • 更新 quote_categories_quote_services.last_quote_sent_on
  • 更新 FETNET 綁定 consumer 的未讀 counter:若 request category 對應 lesson,增加 fetnet_users.unread_lesson;否則增加 fetnet_users.unread_home。沒有 fetnet_users 綁定資料時不影響主流程。
  • 新報價會送 sendReceiveQuoteNotification()
  • 新報價會插入 task_event_queue.new_quote_bid_rating
  • provider 第一筆報價可能觸發 referral check。

付款 / Prime 規則

prime 是 TapPay frontend 取得的一次性付款 token。legacy 在 rating 內使用它做兩種事:

  • wallet 不足時,先寫 unpaid auto-quote debt。
  • 如果累積 debt 達門檻或設定要求立即扣款,有 prime 時用 pay_by_prime 付清 auto quote debt。
  • 如果未達扣款門檻,有 prime 時只做 bind_card
  • 沒有 prime 但有有效 auto quote card 時,先保留 unpaid debt,等後續 debt charge 流程處理。

注意:quote_user_subscription_logs.quote_bid_idint,但新版 quote_bids.id 已可能超過 signed int 上限。測試機已觀察到 quote_bid_id=2147684586 寫入 subscription log 時,quote_user_subscription_logs.quote_bid_id 會變成 2147483647。因此排查 subscription log 時要優先用 quote_bids.quote_user_subscription_log_id -> quote_user_subscription_logs.id,不要只用 quote_user_subscription_logs.quote_bid_id 反查。

重要錯誤:

情境legacy error
未設定信用卡 / 無法完成 card setuperror=10, err_change_status_rating_10
一般扣款失敗或餘額不足常見 error=2, err_change_status_rating_2
TapPay foreign carderror=211, err_change_status_rating_211
credit card cool down回固定訊息「綁定信用卡準備中,請等30秒後再試」
未預期例外error=999, err_change_status_rating_999

回應可能包含:

  • bank_result_code
  • bank_result_msg

Log / Queue

相關 application log channel:

change_status_rating
change_status_rating__checkNewUser
change_status_rating_is_allow_contact_charge_error
change_status__rating
QuoteBids_Rating
quote_bid_rating
rating_request_sent_count
dealUpdateCol_error1
dealUpdateCol_error2
quote_bid_rating_handler

相關 queue / handler:

eventhandler / 用途
task_event_queue.quote_bid_calc_counterQuoteBidCounterCacheHandler,重算 provider / request / service counter
task_event_queue.new_quote_bid_ratingQuoteBidRatingHandler,更新平均回覆時間
task_event_queue.request_bid_count_limitRequestBidCountLimitHandler,正式環境會處理 SEO request ES
event_queue.Receive_Quote新報價通知:MoEngage Receive_Quote、email 110 New Quote Received Notification、push/web push push.content.project.quoted、SMS sms.receive_quote
task_event_queue.request_sent_countcontact charge 轉保證回覆報價時更新 sent count

測試機驗證紀錄

2026-05-11 測試機呼叫:

POST /quote_bids/change_status/rating/2147684586.json
is_contact_charge=0
rating_type=hourly
hourly_rate_at=109
price_note=哈哈囉囉囉ㄌ

response:

{"status":"Success","error":0,"hash_url":"","is_new":1,"minimum_transaction_amount":3}

DB 對照結果:

類型結果
quote_bidsquote_status_id=2, provider_status_id=2, quote_sent_on 已寫入,is_interested=1, is_paid_for_subscription=1, is_contact_charge=0, is_auto_quote=0
報價內容rating_type=hourly 對應 hourly_rate_at=109, quote_amount=109, pricing_unit=小時, price_note=哈哈囉囉囉ㄌ
quote_requestsquote_bid_count=1, is_requestor_readed=0
paymenttransactions.transaction_type_id=40, amount=102, real_pay_date 已被 default card debt charge 補上,stripe_charge_id=D20260511XD6H38
subscription logquote_bids.quote_user_subscription_log_id=20534,但 quote_user_subscription_logs.quote_bid_id=2147483647,原因是欄位型別 int overflow
activityquote_activities.SubmitQuoteprovider_user_id=8617, requestor_user_id=8514, receiver_user_id=8617
task queuequote_bid_calc_counternew_quote_bid_rating 已在 task_event_queue_log 成功
notificationReceive_Quote 已在 event_queue_log_2 成功,包含 email / SMS / web_push / push / MoEngage
contact chargequote_bid_site_feesquote_bid_calls 沒有 row,符合 is_contact_charge=0

相關資料表

類型資料表
bid 狀態 / 報價內容quote_bids
request 名額 / 狀態quote_requests, quote_request_limit_ids
contact callquote_bid_calls
扣款 / 訂閱 logtransactions, quote_user_subscription_logs, stripe_customers, tap_pay_charge_logs
fee / couponquote_bid_site_fees, quote_coupons
activity / messagequote_activities, messages
queuetask_event_queue, task_event_queue_log, event_queue, event_queue_log_1..4
報價模板 / 統計quote_templates, quote_categories_quote_services, quote_categories

建議切批

不要和 invoice / completed / self_hire 同一批。

建議拆成:

  1. 只補 route + endpoint skeleton,確認 routing 與錯誤格式。
  2. 搬前置 guard,先用 unit test 固定 provider / phone / blocked / request closed 行為。
  3. 搬一般手動報價成功 path,不含 contact charge。
  4. 搬付款與 retry path。
  5. 搬 contact charge path。
  6. 補通知、activity、message、template、last quote sent side effect。

建議測試矩陣

最小可先補:

  • 一般手動報價成功。
  • wallet 足額扣款成功。
  • wallet 不足但有 auto quote card 時寫 unpaid auto-quote debt。
  • 已有 quote_sent_on 擋下。
  • provider phone 未驗證。
  • request closed / archived / expired。
  • bid count limit reached。
  • 同 provider 已用其他 bid 對同 request 報價。
  • 一般手動報價無 wallet / 無有效 auto quote card。
  • default card foreign card error=211
  • default card retry / cooldown。
  • contact charge 成功,含 quote_bid_calls
  • contact charge 無 prime 且 provider 未開 auto quote card,回 error=10
  • prime 綁卡 / 付欠款成功。
  • queue-or-log:new_quote_bid_rating、新報價通知事件。