QuoteBids Preview Quote Fee

對應 legacy API GET /quote_bids/preview_quote_fee/{quote_bid_id}/is_auto_quote:{is_auto_quote}.json

注意:quote_bid_idis_auto_quote 都是 path segment。正式 access log 看到的公開 caller 是:

GET /quote_bids/preview_quote_fee/2508514017/is_auto_quote:0.json
GET /quote_bids/preview_quote_fee/2521355641/is_auto_quote:1.json

快速結論

  • API 定位:provider 送出報價前,預覽本次報價 / 聯絡可能產生的 site fee。
  • HTTP path:GET /quote_bids/preview_quote_fee/{quote_bid_id}/is_auto_quote:{is_auto_quote}.json
  • 舊專案入口:get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php::preview_quote_fee($quote_bid_id = null)
  • 新專案 endpoint:Endpoint/V1/QuoteBids.php::preview_quote_fee()
  • 新專案 routing:Lib/Common/RouterRule/Mapping.php explicit mapping 到 QuoteBids::preview_quote_fee()
  • 認證:使用者 session + API key
  • 搬移狀態:已補 action、routing、fee helper 差異、ready coupon 分支、PHPUnit;staging response / DB side effect baseline 已驗證

Request

GET /quote_bids/preview_quote_fee/{quote_bid_id}/is_auto_quote:{is_auto_quote}.json
參數來源說明
quote_bid_idpath要預覽 fee 的 bid。必須屬於目前 session user 的 provider。
is_auto_quotepath named param01;legacy 允許 caller 覆寫本次 fee calculation 使用的 auto quote 判斷。

Legacy 行為

legacy controller 主流程:

QuoteBidsController::preview_quote_fee($quote_bid_id)
-> RestApiHelper::hasValidApiSession()
-> QuoteBid.find(first) by id + provider_user_id + quote_status_id != Removed
-> optional is_auto_quote override
-> QuoteBid::getSiteFee()
-> optional coupon campaign translation
-> optional contact_charge_info
-> json_ajax(response)

成功 response:

{
  "error": 0,
  "original_site_fee": "111",
  "site_fee": "111",
  "title": "",
  "description": "",
  "site_fee_reason": {},
  "contact_charge_info": null
}

錯誤路徑:

條件legacy error
非 JSON request 或 session 無效error=1, status=invalid_request
bid 不存在、非目前 provider、或已 removederror=2, status=bid_does_not_exist

新專案實作

新 action 保持 thin endpoint:

Endpoint/V1/QuoteBids.php::preview_quote_fee()
-> ApiValidation::assertApiSession()
-> load quote_bids + quote_requests
-> apply is_auto_quote override
-> PreviewQuoteFeeUtil::getSiteFee()
-> coupon campaign translation
-> contact_charge_info calculation

新舊程式對照

detail doc 固定要同時保留入口定位與逐段對照:先列出 Legacy PHP 5.6 / New PHP 8.2 的檔案與行號範圍,再用逐段表格照 legacy 執行順序對到 new code。

Legacy PHP 5.6

legacy controller:

/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Controller/QuoteBidsController.php
legacy 行號職責
6300-6306preview_quote_fee($quote_bid_id = null) 入口;檢查 JSON request 與 API session。
6308-6314取得 session user、is_auto_quote named param、語系,建立 QuoteBid model。
6317-6334quote_bid_idprovider_user_id、非 Removed 狀態讀 bid/request;找不到回 bid_does_not_exist
6336-6338若 caller 傳 is_auto_quote,只覆寫本次 fee calculation,不寫回 DB。
6339-6347contact charge bid 讀 quote_bid_site_fees.contact_charge snapshot,再呼叫 QuoteBid::getSiteFee()
6348-6369coupon campaign title / description 多語合併。
6371-6390支援 contact charge 的 category 會再算一次 contact_charge_info
6392-6400成功 response,original_site_fee / site_fee 維持 string 型別。
6402-6405error / unexpected exception response 與 log。

legacy fee helper:

/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.php
legacy 行號職責
3287-3310getSiteFee() input normalization、test fee、failed-card contact charge coupon 邊界。
3312-3336commission mode fee。
3338-3389auto quote:720 小時限制、trial、upfront、direct match。
3391-3451contact charge:fulfilled coupon、ready coupon、general coupon、discounted contact charge。
3453-3495narrow match coupon、new auto quote trial、preserve auto、normal auto fee。
3498-3558manual quote:ready coupon、general coupon、upfront、preserve、discounted manual、normal manual fee。

New PHP 8.2

new 檔案行號對應 legacy職責
Lib/Common/RouterRule/Mapping.php:145-147access log 實際 path/quote_bids/preview_quote_fee/{quote_bid_id}/is_auto_quote:{is_auto_quote}.json route 到 QuoteBids::preview_quote_fee()
Endpoint/V1/QuoteBids.php:2272-2421QuoteBidsController.php:6300-6405endpoint 主流程、session guard、bid/request 查詢、fee preview、contact charge info、response mapping。
Endpoint/V1/QuoteBids.php:2424-2487legacy Cake array shape將 SQL row 轉成 QuoteBidsDO / QuoteRequestsDO,並處理 coupon campaign 多語文字。
Lib/Util/PreviewQuoteFeeUtil.php:39-303QuoteBid.php:3287-3558搬移 QuoteBid::getSiteFee() 的 preview fee calculation。
Lib/Model/QuoteCoupon.php:73-103QuoteCoupon.php:92-124getReadyCoupon() 回傳 legacy ready coupon shape。
tests/QuoteBidsPreviewQuoteFeeTest.phpcontroller response / fee helper branch覆蓋成功 response、非 provider guard、ready coupon 優先順序與 no side effect。

逐段對照

入口 routing 對照:

new 行號對應 legacy職責
Lib/Common/RouterRule/Mapping.php:145-147access log 實際 path GET /quote_bids/preview_quote_fee/{quote_bid_id}/is_auto_quote:{is_auto_quote}.json將 path id 與 Cake named param 轉成 QuoteBids::preview_quote_fee() 的 params。

controller 主流程對照:

new 行號對應 legacy職責
2272-22856300-6306endpoint 入口與 session/API request guard;失敗回 error=1, status=invalid_request
2287-22956308-6311解析 session user、quote_bid_idis_auto_quote named param、語系。
2297-23396313-6334讀取 bid/request,限制 provider_user_id = current userquote_status_id != Removed;找不到時回 error=2, status=bid_does_not_exist
2341-23466336-6338若 caller 帶 is_auto_quote,只覆寫本次 fee calculation 使用的 bid 狀態,不寫回 DB。
2348-23546339-6346contact charge bid 讀取 quote_bid_site_fees 的 frozen fee,覆寫本次 request fee input。
2356-23606347呼叫 fee calculation,取得 site_fee_reasonoriginal_site_fee
2362, 2459-24876348-6369coupon campaign title / description 多語合併,沒有 coupon 時維持空字串。
2364-23936371-6390若 category 支援 contact charge,強制以 is_auto_quote=1is_contact_charge=1 再算一次 contact_charge_info
2395-24036392-6400成功 response payload;金額欄位維持 legacy string 型別。
2404-24096402-6403ProException 相容 error response。
2410-24206404-6405未預期例外寫 quote_bids__preview_quote_fee log。

fee calculation 對照:

Lib/Util/PreviewQuoteFeeUtil.php::getSiteFee()
new 行號對應 legacy職責
39-62QuoteBid.php:3287-3310fee input normalization;is_failed_card_bid = is_contact_charge && is_new_leads;failed-card bid 不套 contact charge test fee,維持 legacy。
64-863312-3336commission mode fee、fallback default fee、deposit_paid
88-1363338-3389auto quote 的 720 小時限制、trial、upfront、direct match。
138-1973391-3451contact charge branch;fulfilled coupon、ready coupon、general contact-charge coupon、discounted contact charge。
167-1873421-3441contact charge 只有在 is_check_contact_charge_coupon && !is_failed_card_bid 時 lock user、先取 ready coupon,再取一般 coupon。
199-2403453-3495narrow match coupon、free auto quote、preserve auto、normal auto fee。
243-2713498-3527manual quote coupon branch;lock user 後先取 ready coupon,再取一般 coupon。
273-2973529-3558manual quote upfront、preserve、discounted manual、normal manual fee。

coupon helper 對照:

Lib/Model/QuoteCoupon.php
new 行號對應 legacy職責
73-103QuoteCoupon.php:92-124getReadyCoupon($bid_id)quote_coupon_ready_bids,回傳 legacy shape:QuoteCoupon.id = "0"QuoteCouponReadyBidQuoteCouponCampaign.discount_*
19-71legacy preview 不 fulfill;legacy contact/payment flow 會用 coupon id 0 表示 ready coupon新版 fulfillCoupon(0, $quoteBidId) 會把 ready coupon materialize 到 quote_coupons,並標記 quote_coupon_ready_bids.is_used = 1;保留 preview 不寫 DB。

主要 helper 對照:

legacynew
QuoteBid::getSiteFee()PreviewQuoteFeeUtil::getSiteFee()
QuoteBidSiteFee::get($id, 'contact_charge')QuoteBidSiteFee::getFee($id, 'contact_charge')
MultiTranslation::getHeaderLangId()MultiTranslation::getHeaderLangId($this->request)
QuoteCategory::isSupportContactCharge()同 model method

這支 API 只做 fee preview,不應寫主要 DB side effect、transaction、activity 或 queue。

測試對照:

tests/QuoteBidsPreviewQuoteFeeTest.php
test 行號對應 legacy職責
40-84QuoteBidsController.php:6392-6400成功 response shape、金額字串型別;確認 preview 不寫 transaction / subscription side effect。
86-956300-6334非 provider session 看同一筆 bid 時回 error=2, status=bid_does_not_exist
97-137QuoteBid.php:3425-3432, 3512-3519ready coupon 優先於一般 coupon,quote_coupon_id = "0",site fee 依 ready coupon 計算。
261-344QuoteCoupon.php:92-124測試 fixture 建立 campaign、ready bid、一般 coupon,覆蓋 legacy ready coupon shape。

Response Payload 對照

response keynew 行號對應 legacy規則
error23966393成功固定 0
original_site_fee23976394必須 strval(),維持字串型別。
site_fee23986395必須 strval(),維持字串型別。
title23996396coupon campaign title;無 coupon 時空字串。
description24006397coupon campaign description;無 coupon 時空字串。
site_fee_reason24016398fee calculation reason 原樣回傳。
contact_charge_info24026399不支援 contact charge 時 null;支援時包含 original_site_fee, site_fee, site_fee_reason

Side Effect 規則

這支 API 沒有 notification payload,也不應新增任何通知事件。和 consumer_want_provider_quote 不同,沒有對應 send*Mail()EventTaskDOEventQueueTaskEventQueue 流程。

Notification payload 對照:

無對應 send*Mail() / EventTaskDO / EventQueue / TaskEventQueue
new 行號對應 legacy職責
fee preview 不建立 email/push/web push/sms payload。
Endpoint/V1/QuoteBids.php:2395-2403QuoteBidsController.php:6392-6400只回同步 preview response,不送 async notification。
tests/QuoteBidsPreviewQuoteFeeTest.php:40-84legacy preview 無 notification write測試確認不寫 transaction / subscription;此 API 沒有 queue 寫入路徑。
類型legacynew規則
DB write無主要寫入無主要寫入不更新 quote_bids.total_site_fee、不建立 subscription log、transaction、activity。
notification不寫 EventQueue / TaskEventQueue
payment只預覽,不扣 wallet、不刷卡、不寫 TapPay / Stripe result。
logunexpected exception 寫 quote_bids__preview_quote_fee同 channel只在 exception path 寫 application log。

正式機 Access Log

2026-05-20 查詢舊專案正式機 Apache access log:

  • 機器:ip-10-5-2-242
  • 檔案:/var/log/apache2/get-lancer_access.log.*.gz
  • pattern:quote_bids/preview_quote_fee(/|\.json)

gzip 舊 log 命中:

41144

path 分布樣本顯示主要 caller 使用 path id + named param:

quote_bids/preview_quote_fee/2510245798/is_auto_quote:0.json
quote_bids/preview_quote_fee/2521355641/is_auto_quote:1.json

tail 樣本包含:

  • GET 200
  • OPTIONS 200
  • iOS app pro360/6.6.0
  • Android okhttp/4.12.0
  • Web browser from https://www.pro360.com.tw/

結論:這支是高流量 API,應列為下一批 quote_bids migration 優先目標。

Verification

已完成:

  • php -l Endpoint/V1/QuoteBids.php
  • php -l Lib/Common/RouterRule/Mapping.php
  • php -l Lib/Model/QuoteCoupon.php
  • php -l Lib/Util/PreviewQuoteFeeUtil.php
  • php -l tests/QuoteBidsPreviewQuoteFeeTest.php
  • docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/QuoteBidsPreviewQuoteFeeTest.php

PHPUnit result:

OK (3 tests, 31 assertions)

Staging Baseline

2026-05-20 使用 staging API 與 DBFactory 回查 baseline。

一般 fee preview

GET /quote_bids/preview_quote_fee/2147688100/is_auto_quote:0.json

response:

{
  "error": 0,
  "original_site_fee": "101",
  "site_fee": "101",
  "title": "",
  "description": "",
  "site_fee_reason": {
    "8": {
      "reason": "Normal",
      "discount": 1
    }
  },
  "contact_charge_info": null
}
GET /quote_bids/preview_quote_fee/2147688100/is_auto_quote:1.json

response:

{
  "error": 0,
  "original_site_fee": "111",
  "site_fee": "111",
  "title": "",
  "description": "",
  "site_fee_reason": {
    "7": {
      "reason": "Normal",
      "discount": 1
    }
  },
  "contact_charge_info": null
}

DB side effect after call:

quote_bids.total_site_fee = 101
quote_bids.quote_user_subscription_log_id = 0
transactions count = 0
quote_user_subscription_logs count = 0
quote_activities count = 0

Contact Charge Info

GET /quote_bids/preview_quote_fee/2147687876/is_auto_quote:0.json

response:

{
  "error": 0,
  "original_site_fee": "88",
  "site_fee": "88",
  "title": "",
  "description": "",
  "site_fee_reason": {
    "8": {
      "reason": "Normal",
      "discount": 1
    }
  },
  "contact_charge_info": {
    "original_site_fee": "140",
    "site_fee": "140",
    "site_fee_reason": {
      "12": {
        "reason": "保證回覆",
        "discount": 1
      }
    }
  }
}
GET /quote_bids/preview_quote_fee/2147687876/is_auto_quote:1.json

response:

{
  "error": 0,
  "original_site_fee": "97",
  "site_fee": "97",
  "title": "",
  "description": "",
  "site_fee_reason": {
    "7": {
      "reason": "Normal",
      "discount": 1
    }
  },
  "contact_charge_info": {
    "original_site_fee": "140",
    "site_fee": "140",
    "site_fee_reason": {
      "12": {
        "reason": "保證回覆",
        "discount": 1
      }
    }
  }
}

DB side effect after call:

quote_bids.total_site_fee = 88
quote_bids.quote_user_subscription_log_id = 0
transactions count = 0
quote_user_subscription_logs count = 0
quote_activities count = 0

Error Baseline

同一筆 bid 使用非 provider session:

GET /quote_bids/preview_quote_fee/2147687876/is_auto_quote:0.json

response:

{"error":2,"status":"bid_does_not_exist"}

待補:

  1. coupon campaign title / description 多語 baseline。
  2. 有實際 ready coupon / fulfilled coupon 的 staging baseline。