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\QuoteBiddomain 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_id與user_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_CONTACT且is_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_id、user_id、call_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_bids | id = quote_bid_id | 更新同一筆 bid。 |
quote_bids | is_want_to_contact_provider | 寫 1。 |
quote_bids | contact_provider_on | 寫目前時間。 |
quote_bids | is_paid_for_subscription | 寫 1。 |
quote_bids | is_interested | 寫 1。 |
quote_bids | reason | 寫 JWT payload 解析後的 reason;預設 TWILIO_CALL,is_pro_search_contact = 1 時預設 PRO_SEARCH_CONTACT,payload 有 reason 時覆寫。 |
quote_bids | requestor_note | payload 有 preserve_note 時寫入。 |
quote_bids | quote_user_subscription_log_id | 若尚未有 subscription log,會寫入新建的 quote_user_subscription_logs.id。 |
quote_bids | total_site_fee | 依聯絡 provider 主線寫入本次 site fee。 |
quote_requests | id = quote_bids.quote_request_id | 若原本尚未 contact 且非 narrow match,auto_quote_contact_count 加 1。 |
quote_user_subscription_logs | 新增 row | 若 bid 尚未有 subscription log,建立本次聯絡扣款 log。 |
transactions | class = QuoteBid, foreign_id = quote_bid_id | 依聯絡扣款主線寫入 quote bid fee / 欠款。 |
quote_activities | secondary_model = QuoteBid, secondary_foreign_id = quote_bid_id, quote_activity_type_id = AutoQuoteContact | 若尚未存在,建立成功聯絡 provider activity。 |
task_event_queue / task_event_queue_log | event_key = bid_contact_count | 寫入 bid contact counter 重算任務;驗證時 queue / log 任一存在即可。 |
task_event_queue / task_event_queue_log | event_key = sent_contact_provider_sms | 非 narrow match 時寫入 SMS 任務;驗證時 queue / log 任一存在即可。 |
event_queue / event_queue_log_* | event_key = Contact_Pro | 寫入 provider notification event;驗證時需查 event_queue 與 event_queue_log_1 到 event_queue_log_4。 |
call_fee_jwt
這支只處理電話費 transaction,不會更新 quote_bids contact 狀態。
| table | 實際欄位 / 條件 | 寫入規則 |
|---|---|---|
transactions | 新增 row | 寫入電話費交易。 |
transactions | user_id | JWT payload 的 user_id,且必須等於 quote_bids.provider_user_id。 |
transactions | foreign_id | JWT payload 的 quote_bid_id。 |
transactions | class | 固定寫 Call。 |
transactions | transaction_type_id | 固定寫 QuotePaymentForSubmissionAutoQuote (40)。 |
transactions | amount | JWT payload 的 call_fee。 |
transactions | real_pay_amount | 同 call_fee。 |
transactions | real_pay_date | 一般情境寫 NULL;月結 auto quote 情境寫目前時間。 |
transactions | stripe_charge_id | 當累積欠款達刷卡條件且刷卡成功時,由 debt charge 流程回填。 |
新舊程式對應
Legacy PHP 5.6
主要入口:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php| legacy 行號 | 職責 |
|---|---|
2032-2061 | want_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-2171 | call_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-3778 | processCallFee():確認 provider、取 payoff lock、交易包住 call fee charge。 |
3780-3890 | processChargeForCall():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.php2026-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_jwtinvalid JWT 回error=2。want_to_contact_provider_for_call_jwt缺quote_bid_id回error=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_82target 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 = 1quote_bids.quote_user_subscription_log_id指到quote_user_subscription_logs.idtransactions.class = QuoteBid且foreign_id = quote_bid_id- 建立 AutoQuoteContact activity
bid_contact_count/sent_contact_provider_sms進入task_event_queue或task_event_queue_logContact_Pro進入event_queue或event_queue_log_1到event_queue_log_4
call_fee_jwt 成功後應確認:
transactions.class = Calltransactions.foreign_id = quote_bid_idtransactions.transaction_type_id = 40transactions.amount = JWT payload.call_fee
注意:手動直接呼叫 call_fee_jwt 只會寫電話費 transaction,不會更新 quote_bid_calls.call_fee。quote_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 = 2147688816want_to_contact_provider_for_call_jwt建立quote_user_subscription_logs.id = 20791,並寫入transactions.id = 36815,class = QuoteBid,amount = 224call_fee_jwt寫入transactions.id = 36816,class = Call,amount = 30- 手動直打後
quote_bid_calls.id = 1328的call_fee仍為0,符合上面 callback 責任邊界
正式機 Access Log
2026-05-25 current log 樣本中,未 normalize path 前可見:
| API | 筆數 |
|---|---|
POST /quote_bids/want_to_contact_provider_for_call_jwt | 12 |
POST /quote_bids/call_fee_jwt | 5 |
這兩支應視為同一組電話聯絡內部 API,一起搬移與驗證。