QuoteBids Call JWT

對應 legacy API:

POST /quote_bids/want_to_contact_provider_for_call_jwt
POST /quote_bids/call_fee_jwt

功能說明

電話聯絡流程在通話有接通或進入可收費條件後,會由 Twilio / Infobip callback 內部呼叫這兩支 API。

want_to_contact_provider_for_call_jwt 用來保證「電話聯絡成功」後,仍會進入聯絡 provider 的扣款與 contact side effect 主線。

call_fee_jwt 用來在通話超過免費秒數後,補收電話接通服務費;它只處理電話費 transaction,不直接建立通話狀態。

操作路徑

consumer 發起電話聯絡後,語音服務 callback 會更新 quote_bid_calls,並依通話結果呼叫內部 API:

Twilio / Infobip callback
-> 通話接通且 DialCallDuration > 0
-> POST /quote_bids/want_to_contact_provider_for_call_jwt

若通話時間超過 180 秒,會再計算電話費:

DialCallDuration > 180
-> call_fee = ceil((min(DialCallDuration, 600) - 180) / 60) * twilio_call_fee
-> POST /quote_bids/call_fee_jwt

備註

這兩支 API 不使用一般 user session;不需要 X-Pro360-User-Session-Token

header 需帶:

Content-Type: application/json
X-Pro360-Rest-Api-Key: <jwt_api_key>

request body 只帶簽章後的 jwt

{"jwt":"xxxxx.yyyyy.zzzzz"}

jwt 使用 jwt_api_key 簽章;以下欄位是在 JWT payload 內,不是裸送 request body。

want_to_contact_provider_for_call_jwt JWT payload:

{
  "quote_bid_id": 2529215105,
  "user_id": 8616,
  "is_pro_search_contact": 1,
  "reason": "PRO_SEARCH_CONTACT",
  "preserve_note": "optional"
}

call_fee_jwt JWT payload:

{
  "quote_bid_id": 2529215105,
  "user_id": 4851,
  "call_minute": 2,
  "call_fee": 30
}

成功 response:

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

錯誤 response:

{"error":2,"message":"invalid_jwt_request"}

更新時間:2026-05-25

快速結論

  • 用途:電話聯絡成功後的內部扣款 / 電話費處理。
  • actor:internal callback / service-to-service JWT,不是一般 user session。
  • new API:已補 Endpoint/V1/QuoteBids.php::want_to_contact_provider_for_call_jwt()Endpoint/V1/QuoteBids.php::call_fee_jwt()
  • helper:目前沿用 PRO360\Model\QuoteBid domain method。
  • 測試:tests/QuoteBidsCallJwtTest.php
  • 測試指令:docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/QuoteBidsCallJwtTest.php
  • 注意:call_fee_jwt 寫入的 transaction 使用 transactions.class = Call,不要和一般 quote bid fee 的 class = QuoteBid 混在一起判讀。

New API routing

這兩支正式 caller 沒有 .json 副檔名,依一般 controller/action routing:

quote_bids/want_to_contact_provider_for_call_jwt
-> Endpoint/V1/QuoteBids.php::want_to_contact_provider_for_call_jwt()
 
quote_bids/call_fee_jwt
-> Endpoint/V1/QuoteBids.php::call_fee_jwt()

目前未新增 explicit Mapping.php route;File router fallback 可進 Endpoint/V1/QuoteBids.php

Response contract

成功:

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

want_to_contact_provider_for_call_jwt JWT invalid / payload 缺欄位:

情境response
invalid JWT{"error":1,"message":"invalid_jwt_request"}
quote_bid_id{"error":1,"message":"empty_quote_bid_id"}
user_id{"error":1,"message":"empty_user_id"}

call_fee_jwt JWT invalid / payload 缺欄位:

情境response
invalid JWT{"error":2,"message":"invalid_jwt_request"}
quote_bid_id{"error":3,"message":"empty_quote_bid_id"}
user_id{"error":4,"message":"empty_user_id"}
call_fee{"error":5,"message":"empty_call_fee"}
user_id 不是 bid provider{"error":6,"message":"not quote_bid provider"}

Legacy 規則

want_to_contact_provider_for_call_jwt

成功條件:

  • JWT 簽章有效。
  • payload 有 quote_bid_iduser_id
  • 可成功執行 QuoteBid::processWantToContactProvider()

成功 side effect:

  • 呼叫 processWantToContactProvider(user_id, quote_bid_id, reason, false, false, preserve_note, null, options)
  • 預設 reason = TWILIO_CALL
  • is_pro_search_contact = 1 時,reason = PRO_SEARCH_CONTACTis_switch_fail = true
  • payload 若有 reason,會覆寫前述 reason。
  • 成功後嘗試寫 Message::quote_conversation_with_requestor_note();失敗只寫 log,不影響成功 response。

失敗補償:

  • 若主流程失敗且 is_switch_fail = false,legacy 會再呼叫一次 processWantToContactProvider(..., add_transaction_only = true),確保電話聯絡已發生時仍建立欠款紀錄。
  • 補償失敗只寫 log,對外 response 仍回原本主流程錯誤。

call_fee_jwt

成功條件:

  • JWT 簽章有效。
  • payload 有 quote_bid_iduser_idcall_fee
  • user_id 必須是該 quote_bids.provider_user_id

成功 side effect:

  • 寫入 transactions.class = Call
  • transactions.foreign_id = quote_bid_id
  • transactions.transaction_type_id = QuotePaymentForSubmissionAutoQuote (40)
  • transactions.amount = call_fee
  • transactions.real_pay_amount = call_fee
  • transactions.real_pay_date = null,除非 provider 是月結 tier。
  • call fee 不走 wallet 即時扣款路徑;legacy QuoteBid::processChargeForCall() 在 call fee 分支只檢查 auto quote card,沒有像一般 contact charge 一樣檢查 available_wallet_amount >= site_fee 後呼叫 consumeUserWallet()。因此 provider 即使 wallet 足夠,call_fee_jwt 仍會寫 transactions.class = Call 的 unpaid auto quote debt;只有有 auto quote card 時,才可能和既有欠款合併後刷卡。

失敗補償:

  • 若主流程失敗,legacy 會再以 add_transaction_only = true 呼叫 processCallFee()
  • 補償成功時會留下未付款 transactions.class = Call 欠款;response 仍回原本主流程錯誤。

資料表變化

want_to_contact_provider_for_call_jwt

這支會進入既有 processWantToContactProvider() 主線,資料變化和 /quote_bids/want_to_contact_provider/{quote_bid_id}.json 相同。

table實際欄位 / 條件寫入規則
quote_bidsid = quote_bid_id更新同一筆 bid。
quote_bidsis_want_to_contact_provider1
quote_bidscontact_provider_on寫目前時間。
quote_bidsis_paid_for_subscription1
quote_bidsis_interested1
quote_bidsreason寫 JWT payload 解析後的 reason;預設 TWILIO_CALLis_pro_search_contact = 1 時預設 PRO_SEARCH_CONTACT,payload 有 reason 時覆寫。
quote_bidsrequestor_notepayload 有 preserve_note 時寫入。
quote_bidsquote_user_subscription_log_id若尚未有 subscription log,會寫入新建的 quote_user_subscription_logs.id
quote_bidstotal_site_fee依聯絡 provider 主線寫入本次 site fee。
quote_requestsid = quote_bids.quote_request_id若原本尚未 contact 且非 narrow match,auto_quote_contact_count 加 1。
quote_user_subscription_logs新增 row若 bid 尚未有 subscription log,建立本次聯絡扣款 log。
transactionsclass = QuoteBid, foreign_id = quote_bid_id依聯絡扣款主線寫入 quote bid fee / 欠款。
quote_activitiessecondary_model = QuoteBid, secondary_foreign_id = quote_bid_id, quote_activity_type_id = AutoQuoteContact若尚未存在,建立成功聯絡 provider activity。
task_event_queue / task_event_queue_logevent_key = bid_contact_count寫入 bid contact counter 重算任務;驗證時 queue / log 任一存在即可。
task_event_queue / task_event_queue_logevent_key = sent_contact_provider_sms非 narrow match 時寫入 SMS 任務;驗證時 queue / log 任一存在即可。
event_queue / event_queue_log_*event_key = Contact_Pro寫入 provider notification event;驗證時需查 event_queueevent_queue_log_1event_queue_log_4

call_fee_jwt

這支只處理電話費 transaction,不會更新 quote_bids contact 狀態。

table實際欄位 / 條件寫入規則
transactions新增 row寫入電話費交易。
transactionsuser_idJWT payload 的 user_id,且必須等於 quote_bids.provider_user_id
transactionsforeign_idJWT payload 的 quote_bid_id
transactionsclass固定寫 Call
transactionstransaction_type_id固定寫 QuotePaymentForSubmissionAutoQuote (40)
transactionsamountJWT payload 的 call_fee
transactionsreal_pay_amountcall_fee
transactionsreal_pay_date一般情境寫 NULL;月結 auto quote 情境寫目前時間。
transactionsstripe_charge_id當累積欠款達刷卡條件且刷卡成功時,由 debt charge 流程回填。

新舊程式對應

Legacy PHP 5.6

主要入口:

/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php
legacy 行號職責
2032-2061want_to_contact_provider_for_call_jwt():驗 JWT、讀 quote_bid_id / user_id / reason。
2064-2082呼叫 processWantToContactProvider(),deadlock / lock timeout 最多重試 5 次。
2084-2091寫 requestor note message;失敗只 log。
2098-2124主流程失敗時的 add_transaction_only 補償。
2144-2171call_fee_jwt():驗 JWT、讀 quote_bid_id / user_id / call_fee
2176-2194呼叫 processCallFee(),deadlock / lock timeout 最多重試 5 次。
2199-2224主流程失敗時的 add_transaction_only 補償。

legacy model:

/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.php
legacy 行號職責
3708-3778processCallFee():確認 provider、取 payoff lock、交易包住 call fee charge。
3780-3890processChargeForCall()add_transaction_only、月結、card、欠款合併與刷卡條件;此處 legacy 明確註解 call fee 不能用儲值金扣,且分支只接受有 auto quote card 的 provider,不會用 wallet 扣款。

New PHP 8.2

new 檔案職責
Endpoint/V1/QuoteBids.php::want_to_contact_provider_for_call_jwt()JWT 驗證、reason / preserve note 解析、重試、補償與 response。
Endpoint/V1/QuoteBids.php::call_fee_jwt()JWT 驗證、call fee 解析、重試、補償與 response。
Lib/Model/QuoteBid.php::processCallFee()provider guard、payoff lock、transaction boundary。
Lib/Model/QuoteBid.php::processChargeForCall()class = Call transaction、月結、auto quote debt threshold charge。
Lib/Model/Transaction.php::logQuoteBidCallTxn()建立 transactions.class = Call

Verification

測試指令:

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

2026-05-25 結果:

tests/QuoteBidsCallJwtTest.php
OK (3 tests, 24 assertions)
 
tests/QuoteBidsWantToContactProviderTest.php
OK (4 tests, 95 assertions)

覆蓋:

  • call_fee_jwt 成功寫入 transactions.class = Call
  • call_fee_jwt invalid JWT 回 error=2
  • want_to_contact_provider_for_call_jwtquote_bid_iderror=1
  • 既有 want_to_contact_provider 主線未被 JWT endpoint 共用 domain method 影響。

後續 staging 實測需補:

  • Twilio / Infobip callback 觸發 want_to_contact_provider_for_call_jwt 成功路徑。
  • 通話超過 180 秒後,call_fee_jwt 產生電話費。
  • 主流程失敗時,add_transaction_only 補償會留下欠款 transaction。

Load Balancer Path

這兩支是電話聯絡 callback 內部 API,沒有 .json suffix。Load Balancer 要用精準 path 切到新專案,不要用 broad path。

建議新增兩條 path condition:

/quote_bids/want_to_contact_provider_for_call_jwt
/quote_bids/call_fee_jwt

不要設定成:

/quote_bids/*

避免把尚未搬移或未驗證的 quote_bids API 一起切到新專案。

Rule order

  • 這兩條 rule 要放在 legacy catch-all /quote_bids/* 或 default legacy target 前面。
  • 不要用 /quote_bids/want_to_contact_provider*,避免誤切 POST /quote_bids/want_to_contact_provider/{quote_bid_id}.json
  • 兩支要視為同一組電話聯絡內部 API,一起切換與驗證;不要只切其中一支。

Target group

  • staging:切到 PHP 8.2 / pro360_api_82 target group。
  • production:staging callback 實測完成後,再用同樣兩條精準 path 切換。

New project routing

新專案不需要另外新增 explicit Mapping.php route。這兩支可透過 File router fallback 進入:

Endpoint/V1/QuoteBids.php::want_to_contact_provider_for_call_jwt()
Endpoint/V1/QuoteBids.php::call_fee_jwt()

切換前檢查

  • 本地或 staging new target 直打兩支 API,確認 response 為 {"status":"success","error":0}
  • 確認 request 使用 X-Pro360-Rest-Api-Key 與 JWT body,不使用 X-Pro360-User-Session-Token
  • 跑測試:
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/QuoteBidsCallJwtTest.php

切換後驗證

want_to_contact_provider_for_call_jwt 成功後應確認:

  • quote_bids.is_want_to_contact_provider = 1
  • quote_bids.quote_user_subscription_log_id 指到 quote_user_subscription_logs.id
  • transactions.class = QuoteBidforeign_id = quote_bid_id
  • 建立 AutoQuoteContact activity
  • bid_contact_count / sent_contact_provider_sms 進入 task_event_queuetask_event_queue_log
  • Contact_Pro 進入 event_queueevent_queue_log_1event_queue_log_4

call_fee_jwt 成功後應確認:

  • transactions.class = Call
  • transactions.foreign_id = quote_bid_id
  • transactions.transaction_type_id = 40
  • transactions.amount = JWT payload.call_fee

注意:手動直接呼叫 call_fee_jwt 只會寫電話費 transaction,不會更新 quote_bid_calls.call_feequote_bid_calls.call_fee 是 Twilio / Infobip callback 在呼叫 call_fee_jwt 前先計算並更新。

2026-05-27 local 實測

127.0.0.1:12351 直打兩支 API 已成功:

POST /quote_bids/want_to_contact_provider_for_call_jwt -> {"status":"success","error":0}
POST /quote_bids/call_fee_jwt -> {"status":"success","error":0}

實測資料:

  • quote_bid_id = 2147688816
  • want_to_contact_provider_for_call_jwt 建立 quote_user_subscription_logs.id = 20791,並寫入 transactions.id = 36815, class = QuoteBid, amount = 224
  • call_fee_jwt 寫入 transactions.id = 36816, class = Call, amount = 30
  • 手動直打後 quote_bid_calls.id = 1328call_fee 仍為 0,符合上面 callback 責任邊界

正式機 Access Log

2026-05-25 current log 樣本中,未 normalize path 前可見:

API筆數
POST /quote_bids/want_to_contact_provider_for_call_jwt12
POST /quote_bids/call_fee_jwt5

這兩支應視為同一組電話聯絡內部 API,一起搬移與驗證。