QuoteBids Preview Quote Fee
對應 legacy API GET /quote_bids/preview_quote_fee/{quote_bid_id}/is_auto_quote:{is_auto_quote}.json。
注意:quote_bid_id 與 is_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.phpexplicit 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_id | path | 要預覽 fee 的 bid。必須屬於目前 session user 的 provider。 |
is_auto_quote | path named param | 0 或 1;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、或已 removed | error=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-6306 | preview_quote_fee($quote_bid_id = null) 入口;檢查 JSON request 與 API session。 |
6308-6314 | 取得 session user、is_auto_quote named param、語系,建立 QuoteBid model。 |
6317-6334 | 依 quote_bid_id、provider_user_id、非 Removed 狀態讀 bid/request;找不到回 bid_does_not_exist。 |
6336-6338 | 若 caller 傳 is_auto_quote,只覆寫本次 fee calculation,不寫回 DB。 |
6339-6347 | contact charge bid 讀 quote_bid_site_fees.contact_charge snapshot,再呼叫 QuoteBid::getSiteFee()。 |
6348-6369 | coupon campaign title / description 多語合併。 |
6371-6390 | 支援 contact charge 的 category 會再算一次 contact_charge_info。 |
6392-6400 | 成功 response,original_site_fee / site_fee 維持 string 型別。 |
6402-6405 | error / unexpected exception response 與 log。 |
legacy fee helper:
/Users/mattsu/Documents/Site/get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.php| legacy 行號 | 職責 |
|---|---|
3287-3310 | getSiteFee() input normalization、test fee、failed-card contact charge coupon 邊界。 |
3312-3336 | commission mode fee。 |
3338-3389 | auto quote:720 小時限制、trial、upfront、direct match。 |
3391-3451 | contact charge:fulfilled coupon、ready coupon、general coupon、discounted contact charge。 |
3453-3495 | narrow match coupon、new auto quote trial、preserve auto、normal auto fee。 |
3498-3558 | manual quote:ready coupon、general coupon、upfront、preserve、discounted manual、normal manual fee。 |
New PHP 8.2
| new 檔案行號 | 對應 legacy | 職責 |
|---|---|---|
Lib/Common/RouterRule/Mapping.php:145-147 | access 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-2421 | QuoteBidsController.php:6300-6405 | endpoint 主流程、session guard、bid/request 查詢、fee preview、contact charge info、response mapping。 |
Endpoint/V1/QuoteBids.php:2424-2487 | legacy Cake array shape | 將 SQL row 轉成 QuoteBidsDO / QuoteRequestsDO,並處理 coupon campaign 多語文字。 |
Lib/Util/PreviewQuoteFeeUtil.php:39-303 | QuoteBid.php:3287-3558 | 搬移 QuoteBid::getSiteFee() 的 preview fee calculation。 |
Lib/Model/QuoteCoupon.php:73-103 | QuoteCoupon.php:92-124 | getReadyCoupon() 回傳 legacy ready coupon shape。 |
tests/QuoteBidsPreviewQuoteFeeTest.php | controller response / fee helper branch | 覆蓋成功 response、非 provider guard、ready coupon 優先順序與 no side effect。 |
逐段對照
入口 routing 對照:
| new 行號 | 對應 legacy | 職責 |
|---|---|---|
Lib/Common/RouterRule/Mapping.php:145-147 | access 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-2285 | 6300-6306 | endpoint 入口與 session/API request guard;失敗回 error=1, status=invalid_request。 |
2287-2295 | 6308-6311 | 解析 session user、quote_bid_id、is_auto_quote named param、語系。 |
2297-2339 | 6313-6334 | 讀取 bid/request,限制 provider_user_id = current user 且 quote_status_id != Removed;找不到時回 error=2, status=bid_does_not_exist。 |
2341-2346 | 6336-6338 | 若 caller 帶 is_auto_quote,只覆寫本次 fee calculation 使用的 bid 狀態,不寫回 DB。 |
2348-2354 | 6339-6346 | contact charge bid 讀取 quote_bid_site_fees 的 frozen fee,覆寫本次 request fee input。 |
2356-2360 | 6347 | 呼叫 fee calculation,取得 site_fee_reason 與 original_site_fee。 |
2362, 2459-2487 | 6348-6369 | coupon campaign title / description 多語合併,沒有 coupon 時維持空字串。 |
2364-2393 | 6371-6390 | 若 category 支援 contact charge,強制以 is_auto_quote=1、is_contact_charge=1 再算一次 contact_charge_info。 |
2395-2403 | 6392-6400 | 成功 response payload;金額欄位維持 legacy string 型別。 |
2404-2409 | 6402-6403 | ProException 相容 error response。 |
2410-2420 | 6404-6405 | 未預期例外寫 quote_bids__preview_quote_fee log。 |
fee calculation 對照:
Lib/Util/PreviewQuoteFeeUtil.php::getSiteFee()| new 行號 | 對應 legacy | 職責 |
|---|---|---|
39-62 | QuoteBid.php:3287-3310 | fee input normalization;is_failed_card_bid = is_contact_charge && is_new_leads;failed-card bid 不套 contact charge test fee,維持 legacy。 |
64-86 | 3312-3336 | commission mode fee、fallback default fee、deposit_paid。 |
88-136 | 3338-3389 | auto quote 的 720 小時限制、trial、upfront、direct match。 |
138-197 | 3391-3451 | contact charge branch;fulfilled coupon、ready coupon、general contact-charge coupon、discounted contact charge。 |
167-187 | 3421-3441 | contact charge 只有在 is_check_contact_charge_coupon && !is_failed_card_bid 時 lock user、先取 ready coupon,再取一般 coupon。 |
199-240 | 3453-3495 | narrow match coupon、free auto quote、preserve auto、normal auto fee。 |
243-271 | 3498-3527 | manual quote coupon branch;lock user 後先取 ready coupon,再取一般 coupon。 |
273-297 | 3529-3558 | manual quote upfront、preserve、discounted manual、normal manual fee。 |
coupon helper 對照:
Lib/Model/QuoteCoupon.php| new 行號 | 對應 legacy | 職責 |
|---|---|---|
73-103 | QuoteCoupon.php:92-124 | getReadyCoupon($bid_id) 讀 quote_coupon_ready_bids,回傳 legacy shape:QuoteCoupon.id = "0"、QuoteCouponReadyBid、QuoteCouponCampaign.discount_*。 |
19-71 | legacy 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 對照:
| legacy | new |
|---|---|
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-84 | QuoteBidsController.php:6392-6400 | 成功 response shape、金額字串型別;確認 preview 不寫 transaction / subscription side effect。 |
86-95 | 6300-6334 | 非 provider session 看同一筆 bid 時回 error=2, status=bid_does_not_exist。 |
97-137 | QuoteBid.php:3425-3432, 3512-3519 | ready coupon 優先於一般 coupon,quote_coupon_id = "0",site fee 依 ready coupon 計算。 |
261-344 | QuoteCoupon.php:92-124 | 測試 fixture 建立 campaign、ready bid、一般 coupon,覆蓋 legacy ready coupon shape。 |
Response Payload 對照
| response key | new 行號 | 對應 legacy | 規則 |
|---|---|---|---|
error | 2396 | 6393 | 成功固定 0。 |
original_site_fee | 2397 | 6394 | 必須 strval(),維持字串型別。 |
site_fee | 2398 | 6395 | 必須 strval(),維持字串型別。 |
title | 2399 | 6396 | coupon campaign title;無 coupon 時空字串。 |
description | 2400 | 6397 | coupon campaign description;無 coupon 時空字串。 |
site_fee_reason | 2401 | 6398 | fee calculation reason 原樣回傳。 |
contact_charge_info | 2402 | 6399 | 不支援 contact charge 時 null;支援時包含 original_site_fee, site_fee, site_fee_reason。 |
Side Effect 規則
這支 API 沒有 notification payload,也不應新增任何通知事件。和 consumer_want_provider_quote 不同,沒有對應 send*Mail()、EventTaskDO、EventQueue 或 TaskEventQueue 流程。
Notification payload 對照:
無對應 send*Mail() / EventTaskDO / EventQueue / TaskEventQueue| new 行號 | 對應 legacy | 職責 |
|---|---|---|
| 無 | 無 | fee preview 不建立 email/push/web push/sms payload。 |
Endpoint/V1/QuoteBids.php:2395-2403 | QuoteBidsController.php:6392-6400 | 只回同步 preview response,不送 async notification。 |
tests/QuoteBidsPreviewQuoteFeeTest.php:40-84 | legacy preview 無 notification write | 測試確認不寫 transaction / subscription;此 API 沒有 queue 寫入路徑。 |
| 類型 | legacy | new | 規則 |
|---|---|---|---|
| DB write | 無主要寫入 | 無主要寫入 | 不更新 quote_bids.total_site_fee、不建立 subscription log、transaction、activity。 |
| notification | 無 | 無 | 不寫 EventQueue / TaskEventQueue。 |
| payment | 無 | 無 | 只預覽,不扣 wallet、不刷卡、不寫 TapPay / Stripe result。 |
| log | unexpected 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 命中:
41144path 分布樣本顯示主要 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.jsontail 樣本包含:
GET200OPTIONS200- 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.phpphp -l Lib/Common/RouterRule/Mapping.phpphp -l Lib/Model/QuoteCoupon.phpphp -l Lib/Util/PreviewQuoteFeeUtil.phpphp -l tests/QuoteBidsPreviewQuoteFeeTest.phpdocker 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.jsonresponse:
{
"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.jsonresponse:
{
"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 = 0Contact Charge Info
GET /quote_bids/preview_quote_fee/2147687876/is_auto_quote:0.jsonresponse:
{
"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.jsonresponse:
{
"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 = 0Error Baseline
同一筆 bid 使用非 provider session:
GET /quote_bids/preview_quote_fee/2147687876/is_auto_quote:0.jsonresponse:
{"error":2,"status":"bid_does_not_exist"}待補:
- coupon campaign title / description 多語 baseline。
- 有實際 ready coupon / fulfilled coupon 的 staging baseline。