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_providerconsumer 正式聯絡 provider,系統成立 contact
provider_accept_narrow_matchprovider 接受 consumer 的 narrow match invitation,轉成正式 contact
auto quote / new leads contactprovider 接觸新客源

付款主體是 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_id

provider_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 branchwallet / card / prime 是否可扣processWantToContactProvider log
transaction write交易紀錄是否能寫入logQuoteBidTxn log、transactions
bid statecontact 是否成立quote_bids contact 欄位

Redis 設定來源見 Redis

主要資料表

階段資料表重點欄位
判斷 bidquote_bidsid, provider_user_id, quote_request_id, total_site_fee, is_auto_quote, is_contact_charge
聯絡狀態quote_bidsis_want_to_contact_provider, contact_provider_on, quote_user_subscription_log_id, is_paid_for_subscription
provider walletusersavailable_wallet_amount
provider cardstripe_customersauto quote / valid / default customer 狀態
transactiontransactionsclass = QuoteBid, foreign_id = quote_bid_id, transaction_type_id, real_pay_date, stripe_charge_id
TapPayTapPay logs / application logsrec_trade_id, bank result, pay-by-prime response
activity / queuequote_activities, event_queue, task_event_queuecontact 成功後的 downstream side effect

扣款分支

條件行為主要 side effect
wallet 足夠直接扣 provider wallettransaction 已付款,real_pay_date 有值
wallet 不足,已有可用 auto quote card走既有卡 / auto quote debt 流程transaction 依既有 card charge 結果更新
wallet 不足,無 card,無 prime內部 charge error 16,對外通常轉成要求設定信用卡bid 不應轉成已聯絡成功
wallet 不足,有 primeTapPay pay-by-prime 扣本次 auto quote debttransaction 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 判讀。

內部錯誤意義常見對外結果
16charge error / 無可用付款方式請先設定信用卡
211外國卡或卡片限制請使用國內發行的信用卡
wallet error 102credit not enoughwallet 不足,需 card / prime 補足

API response 只代表對外訊息,不一定是內部根因。要看 application log 才能區分 wallet、card、TapPay、transaction 哪一段失敗。

排查順序

付款或 contact 異常時,不要先猜 table state。先看 log,再回查 DB。

  1. application log
channel = processWantToContactProvider
channel = logQuoteBidTxn
channel = error_in_consume_user_wallet
channel = TapPayUtil_pay_by_prime
  1. quote_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>;
  1. 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;
  1. TapPay / card logs

看 pay-by-prime 是否有 status = 0、是否有 rec_trade_id、是否有 bank_result_code / bank_result_msg

  1. 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,也可能存 TapPay rec_trade_id
  • no-prime 失敗和 prime 成功可以發生在同一筆 bid 的前後兩次 request;第一次失敗不應留下 accepted/contacted 狀態。
  • worker 搬 queue 不代表主流程失敗;主流程 correctness 先看 bid、transaction、activity。