quote_bid contact charge
文件狀態:草稿 / 部分已對 new API / provider_accept_narrow_match 分支已實測
最後驗證:2026-05-07
來源:Lib/Model/QuoteBid.php contact charge trace、provider_accept_narrow_match PHPUnit / staging curl、application log triage 經驗;待補完整金流 provider 來源
目的
quote_bid contact charge 是 provider 因「正式接觸一筆 quote / new lead」而產生的扣款流程。
這不是單一 API 的規則,而是多個 quote bid 流程共用的付款主線。常見入口包含:
| 入口流程 | 為什麼會進扣款 |
|---|---|
want_to_contact_provider | consumer 正式聯絡 provider,系統成立 contact |
provider_accept_narrow_match | provider 接受 consumer 的 narrow match invitation,轉成正式 contact |
| auto quote / new leads contact | provider 接觸新客源 |
付款主體是 provider,不是 consumer。consumer 發出 invitation 或聯絡意圖後,provider 一旦正式接觸新客源,就會依 quote bid fee / wallet / card 狀態進入扣款或交易紀錄流程。
核心 trace
新專案常見主線:
API endpoint
-> Lib/Model/QuoteBid.php::processWantToContactProvider()
-> Redis payoff lock
-> Lib/Model/QuoteBid.php::processWantToContactCharge()
-> wallet / card / TapPay prime branch
-> transactions
-> quote_bids.quote_user_subscription_log_idprovider_accept_narrow_match 會先進:
Lib/Model/QuoteBid.php::processProviderAcceptNarrowMatch()成功 accept 後再轉進同一條 processWantToContactProvider() 主線。
Redis payoff lock
contact charge 會使用 Redis payoff lock 來避免同一 provider 在短時間內重複扣款或併發處理。
這層只代表「併發保護」,不代表扣款成功:
拿到 lock
-> 才能繼續跑 wallet / card / TapPay / transaction
拿不到 lock
-> 可能被視為重複操作或進入 retry / guard
Redis 斷線
-> 可能有 getCheckConnect log,需看後續是否 fallback 或失敗排查時要分清楚:
| 層級 | 代表什麼 | 主要看哪裡 |
|---|---|---|
| Redis lock | 併發 / 重複扣款保護 | getCheckConnect log、Redis key TTL |
| payment branch | wallet / card / prime 是否可扣 | processWantToContactProvider log |
| transaction write | 交易紀錄是否能寫入 | logQuoteBidTxn log、transactions |
| bid state | contact 是否成立 | quote_bids contact 欄位 |
Redis 設定來源見 Redis。
主要資料表
| 階段 | 資料表 | 重點欄位 |
|---|---|---|
| 判斷 bid | quote_bids | id, provider_user_id, quote_request_id, total_site_fee, is_auto_quote, is_contact_charge |
| 聯絡狀態 | quote_bids | is_want_to_contact_provider, contact_provider_on, quote_user_subscription_log_id, is_paid_for_subscription |
| provider wallet | users | available_wallet_amount |
| provider card | stripe_customers | auto quote / valid / default customer 狀態 |
| transaction | transactions | class = QuoteBid, foreign_id = quote_bid_id, transaction_type_id, real_pay_date, stripe_charge_id |
| TapPay | TapPay logs / application logs | rec_trade_id, bank result, pay-by-prime response |
| activity / queue | quote_activities, event_queue, task_event_queue | contact 成功後的 downstream side effect |
扣款分支
| 條件 | 行為 | 主要 side effect |
|---|---|---|
| wallet 足夠 | 直接扣 provider wallet | transaction 已付款,real_pay_date 有值 |
| wallet 不足,已有可用 auto quote card | 走既有卡 / auto quote debt 流程 | transaction 依既有 card charge 結果更新 |
wallet 不足,無 card,無 prime | 內部 charge error 16,對外通常轉成要求設定信用卡 | bid 不應轉成已聯絡成功 |
wallet 不足,有 prime | TapPay pay-by-prime 扣本次 auto quote debt | transaction real_pay_date 有值,stripe_charge_id = rec_trade_id |
prime 是 TapPay 前端 SDK 產生的一次性付款 token,不是 boolean、不是信用卡號,也不是已儲存的卡。它代表「使用者這次在前端輸入或選擇的信用卡資料,TapPay 已先 tokenize,後端可以拿這個 token 做一次綁卡或扣款」。
有帶 prime 時:
- 後端可以用這次的
prime幫 provider 建立或更新付款方式。 - 如果 provider 目前有 auto quote debt,流程會優先用這個
prime嘗試付清欠款。 - 如果沒有需要立即扣的 debt,通常是用來綁定 auto quote card,讓後續報價或聯絡流程可用信用卡扣款。
prime只能使用一次,不應存起來重複刷。
沒有帶 prime 只代表這次 request 沒有新的 TapPay token;如果 provider 既有付款能力不足,流程通常會回「請先設定信用卡」類錯誤。
沒有帶 prime 時:
- 後端不能憑空新增卡片,只能檢查 provider 是否已有可用 auto quote card。
- 如果已有可用 auto quote card,流程可以繼續,必要時用既有 card / customer token 處理扣款或 debt。
- 如果 provider wallet 不足,且沒有可用 auto quote card,就會拒絕需要信用卡保障的流程。
TapPay pay-by-prime 成功時,transactions.stripe_charge_id 會存 TapPay rec_trade_id。欄位名稱沿用舊 transaction schema,不代表一定是 Stripe。
成功狀態
正式 contact / charge 成功後,常見 quote_bids 變化:
quote_bids.is_want_to_contact_provider = 1
quote_bids.contact_provider_on = NOW()
quote_bids.is_paid_for_subscription = 1
quote_bids.quote_user_subscription_log_id = transactions.id若是 narrow match accept,還會看到:
quote_bids.narrow_status_id = ACCEPTED
quote_bids.quote_status_id = InProgress
quote_bids.provider_status_id = InProgress
quote_bids.quote_sent_on = NOW()成功後通常也會有:
transactions.class = QuoteBid
transactions.foreign_id = quote_bid_id
quote_activities.AutoQuoteContact
task_event_queue.bid_contact_count常見錯誤 mapping
不同入口 API 會包裝成不同 message key,但內部付款錯誤可先用 code 判讀。
| 內部錯誤 | 意義 | 常見對外結果 |
|---|---|---|
16 | charge error / 無可用付款方式 | 請先設定信用卡 |
211 | 外國卡或卡片限制 | 請使用國內發行的信用卡 |
wallet error 102 | credit not enough | wallet 不足,需 card / prime 補足 |
API response 只代表對外訊息,不一定是內部根因。要看 application log 才能區分 wallet、card、TapPay、transaction 哪一段失敗。
排查順序
付款或 contact 異常時,不要先猜 table state。先看 log,再回查 DB。
- application log
channel = processWantToContactProvider
channel = logQuoteBidTxn
channel = error_in_consume_user_wallet
channel = TapPayUtil_pay_by_primequote_bids
SELECT
qb.id,
qb.quote_request_id,
qb.provider_user_id,
qb.quote_service_id,
qb.is_auto_quote,
qb.is_contact_charge,
qb.is_want_to_contact_provider,
qb.contact_provider_on,
qb.quote_sent_on,
qb.total_site_fee,
qb.quote_user_subscription_log_id,
qb.is_paid_for_subscription,
qb.narrow_status_id,
qb.quote_status_id,
qb.provider_status_id
FROM quote_bids qb
WHERE qb.id = <quote_bid_id>;transactions
SELECT
id,
created,
user_id,
foreign_id,
class,
transaction_type_id,
amount,
real_pay_amount,
real_pay_date,
stripe_charge_id,
wallet_balance
FROM transactions
WHERE class = 'QuoteBid'
AND foreign_id = <quote_bid_id>
ORDER BY id DESC;- TapPay / card logs
看 pay-by-prime 是否有 status = 0、是否有 rec_trade_id、是否有 bank_result_code / bank_result_msg。
- downstream side effect
event_queue / task_event_queue 可能已被 worker 搬到 log table。驗 downstream 時要接受 queue 或 log 任一存在:
event_queue OR event_queue_log_1..event_queue_log_4
task_event_queue OR task_event_queue_log判讀原則
quote_bids.quote_user_subscription_log_id指向扣款或交易紀錄,不等於一定已刷卡成功;要搭配transactions.real_pay_date判斷。transactions.stripe_charge_id可能存 Stripe charge id,也可能存 TapPayrec_trade_id。- no-prime 失敗和 prime 成功可以發生在同一筆 bid 的前後兩次 request;第一次失敗不應留下 accepted/contacted 狀態。
- worker 搬 queue 不代表主流程失敗;主流程 correctness 先看 bid、transaction、activity。