SearchAdd Legacy Rule Matrix

更新日期:2026-08-10

這份文件是 legacy rule ledger。每條規則都要能追到 legacy source、current owner 與驗證狀態。

閱讀方式:

  1. 先看 Summary,找 rule 與狀態。
  2. 再看 Rule Details,確認 source、side effect、rollback 邊界。
  3. 若要改 current,先把對應 rule 補到至少 legacy_traced

狀態定義與 phase 順序見 legacy_canonical_rules.md

Summary

000-500 Request Materialization

Rule IDPhaseRuleStatusOwnerVerification
SA-LEGACY-0001000raw request / form normalizationneeds_legacy_reauditSearchAddRequestNormalizer, SearchAddInputSearchAddInputTest, SearchAddStep2IntegrationTest
SA-LEGACY-0101100session / credential actor resolutiongapCredentialActorResolver, SearchAddLineIdentityRepositoryLINE 已由 fb8897d7 完成;device-only pending
SA-LEGACY-0102100auto signup and login side effectsgapAutoSignupService, ApiSession, UserLoginLINE link 已由 fb8897d7 完成;existing-session reuse pending
SA-LEGACY-0201200pre-create category / form guardsneeds_legacy_reauditSearchAddHandlerSearchAddStep2IntegrationTest
SA-LEGACY-0301300match type normalization and fee preparationneeds_legacy_reauditQuoteRequestCreator, RequestFeeUpdaterSearchAddStep2IntegrationTest
SA-LEGACY-0401400quote request materializationneeds_legacy_reauditQuoteRequestCreatorSearchAddStep2IntegrationTest
SA-LEGACY-0501500form submission and summary writersgapFormSubmissionWriter, SearchAddHandlerinner_text tests pending
SA-LEGACY-0502500tracking / abuse / abnormal scan writersneeds_legacy_reauditSearchAddHandlerSearchAddStep4IntegrationTest

600-900 Matching, Special Flows, Async

Rule IDPhaseRuleStatusOwnerVerification
SA-LEGACY-0601600auto quote candidate source and filter orderneeds_legacy_reauditAutoQuoteDispatcher, AutoQuoteMatcherSearchAddStep5IntegrationTest
SA-LEGACY-0602600narrow fallback and related-category fillneeds_legacy_reauditNarrowMatchBuilderSearchAddStep5IntegrationTest
SA-LEGACY-0701700bid row finalize, pricing, activity, countersneeds_legacy_reauditQuoteBidFinalizer, BidPricingResolverSearchAddStep6IntegrationTest
SA-LEGACY-0801800preserve provider branchneeds_legacy_reauditSpecialFlowDispatcher, AutoQuoteMatcherSearchAddStep7IntegrationTest
SA-LEGACY-0802800authorized contact branchverifiedAuthorizedMatchProcessor, AuthorizedContactProcessor, QuoteBid::processWantToContactProviderSearchAddStep7IntegrationTest, QuoteBidsWantToContactProviderTest
SA-LEGACY-0803800direct reserve branchneeds_legacy_reauditDirectReserveProcessor, QuoteBid::processWantToContactProviderSearchAddStep7IntegrationTest
SA-LEGACY-0804800direct user branchneeds_legacy_reauditDirectUserProcessor, QuoteBid::processWantToContactProviderSearchAddStep7IntegrationTest
SA-LEGACY-0805800booking branchneeds_legacy_reauditSpecialFlowDispatcherSearchAddStep8IntegrationTest
SA-LEGACY-0901900second-link manual / related async fallbackneeds_legacy_reauditQuoteRequest::checkR1ManualMatchStartNoAutoQuote, NewQuoteRequestHandlerSearchAddStep9IntegrationTest

1000-1100 Response And Shared Downstream

Rule IDPhaseRuleStatusOwnerVerification
SA-LEGACY-10011000public response and error mappingneeds_legacy_reauditEndpoint/V1/SearchAdd.phpSearchAddStep2IntegrationTest, SearchAddStep7IntegrationTest
SA-LEGACY-11011100shared contact provider payment / transaction / notification methodverifiedQuoteBid::processWantToContactProviderQuoteBidsWantToContactProviderTest, QuoteBidsProviderAcceptNarrowMatchTest, SearchAddStep7IntegrationTest

Rule Details

SA-LEGACY-0101 Session / Credential Actor Resolution

Status: gap

Legacy Source

FileLinesRule
/Users/mattsu/Documents/Site/get-lancer-docker-2/get-lancer/app/Plugin/Quotes/Controller/QuoteRequestsController.php2061-2111social input 是互斥 chain:Google → Facebook → Facebook limited → Apple → LINE。
同上2122-2188linked social actor、email mismatch、password、device、LINE same-email 綁定。
同上2355-2378actor failure response。
app/Model/LineVerifyLog.php13-17完整 id token 精確查 verify log;無 expiry / status / order guard。
app/Model/LineUser.php13-52sub 找 user、user link guard、line_users insert。

Current Legacy Rules

  • User[line_id_token] 不替 request 補 email;request email missing 時回 error=11
  • linked LINE user lookup 不加 users.is_active=1
  • token sub 未綁、但 token email 等於既有 user email 時,不需 password / device,即採用 user 並新增 link。
  • token 已綁不同 email:無 password 回 error=11;有 password 回 error=3
  • device id 相同不再設定 user id,因此 device-only 最後回 error=2
  • LINE success 不增加 response 欄位。

Current Mapping / Gap

AreaPHP 8 currentGap
inputSearchAddInput.php:318-335LINE getter 與「前序 social input 未出現才處理」已實作。
identityCredentialActorResolver.php:23-155,259-275,407-425,467-492SearchAddLineIdentityRepository.php:15-109LINE actor / link 已實作;不改這次範圍外的既有 social 行為。
deviceCredentialActorResolver.php:116-130仍允許 device-only actor,尚未對齊。
OAuth precursorEndpoint/V1/Line.php:27-190verify log 已存在並由 search_add 沿用,沒有重打 LINE verify API。

Verification:SearchAddInputTest 9/24、CredentialActorResolverTest 12/62、LINE repository integration 2/20 均通過;成功 actor 的 user_id 皆驗證為 string。Next step 是 device-only negative test;若要保留 current 行為,先記 migration decision。

SA-LEGACY-0102 Auto Signup And Login Side Effects

Status: gap

Legacy Source

FileLinesRule
QuoteRequestsController.php2206-2282建 user 後,以 user id 寫 line_users
同上2343-2347user/device 已有 session 時原 token reuse。
同上3147-3152session 不存在才建立。
app/Model/ApiSession.php34-80新 session 寫 api_sessionsapi_session_logs.action=login
同上102-140rotate 是 shared upsert 行為;正常 search_add 因先 load existing 而不會到達。

Side Effects

line_users explicit fields:created/user_id/line_user_id/name/email;Legacy 不寫 refresh_token

api_session_logs explicit fields:created/user_id/params/session_id/device_id/api_key/actionaction=loginadmin_user_id 保持 default。

Current Mapping / Gap

  • AutoSignupService.php:29-115,451-467 已在 user insert 後依 Legacy allowlist 寫 line_users;integration test 2/20 通過。
  • CredentialActorResolver.php:144-146,421-425AutoSignupService.php:271-292 仍呼叫 shared ApiSession::upsert()
  • merge 後的 ApiSession.php:66-140 會依 user/device/API key 對同 app token寫 exist_logout、刪除、重建並寫 login;新 session login log 已存在。
  • Legacy search_add 對 existing user/device session 是原 token reuse,因此仍需 search_add scoped find-or-create;不可改 shared upsert或套用 rotate。
  • php -d output_buffering=4096 ... SearchAddStep8IntegrationTest.php 為 9/93 通過;一般 PHPUnit CLI 會因 shared session header warning 出現 1 error,測試執行相容性仍待處理。

SA-LEGACY-0501 Form Submission And Summary Writers

Status: gap

NewStaging inner_text Source

FileLinesRule
QuoteRequestsController.php1897-1914pre-create category validation 仍看完整 Form。
同上2896-2918active / same-category inner_textquote_request_inners 後從 Form unset。
同上2925-2931unset 後才建立一般 form submission。

Contract

  • isset():missing / NULL 不寫;空字串與 "0" 會寫。
  • insert fields:created/quote_category_id/quote_request_id/quote_form_field_id/input_value
  • 不進 quote_form_submission_fields,也不參與後續 summary reload、variable fee、abnormal scan、special flow、matching。
  • request create 前的完整 Form 與已提供的 FormSummary 不回溯修改。

Current Mapping / Gap

  • FormSubmissionWriter.php:18-74 目前把 inner field 當一般 field 寫入。
  • 只 skip submission 不足;RequestFeeUpdaterAbnormalScanWriter、special flow 與 matcher仍讀同一份 Form。
  • 應在 request create 後寫 quote_request_inners,再以 filtered SearchAddInput 交給所有 downstream owner。
  • 本地 DB 已核對 active fixture field 14293 / category 614;測試不可假設其他環境 id 相同。

SA-LEGACY-0802 Authorized Contact Branch

Status: verified

這條規則只描述 search_add authorized caller。contact / payment shared method 的完整 contract 見 SA-LEGACY-1101

Legacy Source

FileSymbolLines
../get-lancer-php56/app/Plugin/Quotes/Model/QuoteService.php_handleAutoQuoteMatch2570, 2864-2866, 2908-2915, 2981-2985, 3160-3165, 3233-3250
../get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.phpprocessWantToContactProvider4106-4474

Preconditions

ConditionLegacy rule
request match_typefinal quote_requests.match_type = Authorize
candidate source_handleAutoQuoteMatch() receives auto quote candidate services
candidate countauthorized mode uses the first accepted auto quote candidate, then stops
fee sourceauthorized bid uses quote_requests.direct_fee as total_site_fee before contact
no candidatelogs Operation and sends BD email

Legacy Order

StepLinesRule
12570Determine authorized mode from final request row match_type.
22864-2866Shuffle auto quote candidates in authorized mode.
32908-2915Use direct_fee; skip candidate if category balance is lower than this fee.
42940-2963Build one auto quote bid payload with total_site_fee = direct_fee.
52981-2985Record authorized match and break after the first accepted candidate.
62991-2995Update quote_requests.auto_quote_sent_count.
73107-3165Insert bid, run handleAutoQuote(), capture first bid id, commit.
83178-3203After commit, modify category balance and update auto_quote_last_matched; exceptions are logged, not main-flow fatal.
93233-3235If a bid exists, call processWantToContactProvider($user_id, $last_quote_bid_id, "特約獨佔").
103238-3250If no auto quote was sent, write Operation log and send BD email.

Current Mapping

Legacy areaCurrent ownerVerification
category clamp before authorizedQuoteRequestCreator::normalizeMatchType, SpecialFlowDispatcher::resolveMatchTypetestAuthorizedModeDoesNotRunWhenCategoryClampsMatchTypeToMultiple
specified provider bid creationAuthorizedMatchProcessor::processauthorized Step7 tests create a bid for payload quote_service_id
authorized fee sourceAuthorizedMatchProcessor::process initializes bid from quote_requests.direct_fee; contact mainline charges the same direct feetestAuthorizedModeContactsFirstMatchedProvider covers auto_fee != direct_fee, bid total_site_fee, subscription log amount, and transaction amount
first bid contactAuthorizedContactProcessor::process loads first bid by id ASCtestAuthorizedModeContactsFirstMatchedProvider
no-match notificationAuthorizedContactProcessor::notifyAuthorizedNoMatch writes Operation log and enqueues BD emailtestAuthorizedModeNoMatchWritesOperationLogAndBdEmail
contact / payment side effectsQuoteBid::processWantToContactProvidercovered by SA-LEGACY-1101
reasonSearchAddInput::getAuthorizeReason() with fallback 特約獨佔contact event / reason covered indirectly by authorized tests
no payment methodshared method no-card/no-prime guardtestAuthorizedModeFailsWhenProviderHasNoPaymentMethod
card success / debt only / auto refillshared method and Stripe helperstestAuthorizedModeCreatesDebtTransactionWhenCardExistsButChargeDebtDisabled, testAuthorizedModeChargesDebtImmediatelyWhenCardChargeSucceeds, testAuthorizedModeAutoRefillPaysOffDebtWhenPackageChargeSucceeds
card failure fallbackshared method charge failure handlingtestAuthorizedModeFailsWhenCardChargeFails, testAuthorizedModeFailsWhenCardChargeIsCoolingDown, testAuthorizedModeFailsWhenCardChargeNeedsRetry
foreign cardshared method bank-result mappingtestAuthorizedModeFailsWhenCardIsForeign
retry next cardshared method retry looptestAuthorizedModeRetriesNextCardWhenRetryCreditCardFails
guard failuresshared method contact guardstestAuthorizedModeFailsWhenRequestorIsDisableMatchUser, testAuthorizedModeFailsWhenProviderAndRequestorAreBlockedInChat

Gaps

GapDetailsNext step
noneAudited authorized caller gaps are closed.Keep future changes tied to the tests listed in this rule and to SA-LEGACY-1101 for shared contact/payment behavior.

Verified Fixes

AreaResultVerification
authorized fee sourceAuthorizedMatchProcessor now initializes authorized bid total_site_fee from quote_requests.direct_fee. The shared contact method also resolves authorized charge from direct_fee when match_type = Authorize and auto_quote_sent_count = 1.tests/SearchAddStep7IntegrationTest.php --filter testAuthorizedModeContactsFirstMatchedProvider uses auto_fee = 109, direct_fee = 333, and verifies request fee, bid total_site_fee, quote_user_subscription_logs.amount, and transactions.amount are aligned to 333.
no-match notificationAuthorizedContactProcessor no longer writes the unconsumed Authorized_Mode_No_Match event. It matches legacy by writing monolog_error.channel = Operation with the admin match-check link and enqueuing a BD email through email_queue.tests/SearchAddStep7IntegrationTest.php --filter testAuthorizedModeNoMatchWritesOperationLogAndBdEmail verifies no bid is created, Operation log contains the match-check link, and bdEmailList receives the email.

Current Status

The authorized caller-specific gaps are closed. Successful contact/payment behavior remains delegated to SA-LEGACY-1101, and authorized no-match behavior is verified at the caller level.

SA-LEGACY-1101 Shared Contact Provider Method

Status: verified

這條規則覆蓋 search_add special flow 呼叫 shared contact/payment method 後的完整 downstream contract。只對照 special flow caller 會漏掉 payment、transaction、notification 與 external event。

Legacy Source

FileSymbolLines
../get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.phpprocessWantToContactProvider4106-4474
../get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.phpprocessCharge3572
../get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.phpchargeByCreditCard3895
../get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.phpcompleteChargeByCreditCard3987
../get-lancer-php56/app/Plugin/Quotes/Model/QuoteBid.phpswitchToNarrowMatch4509-4563

Preconditions

Input / OptionLegacy default / meaning
existing quote bidrequestor wants to contact provider through this bid
is_switch_faildefault true; final payment failure may switch to new leads / narrow
add_transaction_onlydefault false; true only logs transaction without card charge
add_contact_activitydefault true; controls AutoQuoteContact activity
is_direct_reservedefault false; suppresses general provider notification when true
is_check_budgetdefault true; controls weekly budget undercharge for normal auto quote contact

Legacy Order

StepLinesRule
14109-4113Read options and defaults.
24124-4129Acquire payoff lock by user_id; lock failure throws code 11.
34138-4143Enter retry loop, up to 10 attempts; begin DB transaction unless in_transaction=true.
44144-4158Narrow accept path first converts bid to auto quote / accepted / in-progress state.
54161-4186Run basicAutoQuoteChecking() and blocked / disabled / blocked-chat guards. Service archived check follows is_narrow_match.
64212-4217Narrow accept creates NarrowProviderAccept, then clears reason for later contact activity.
74219-4235Load quote request and reject archived request.
84241-4311If no subscription log: fee calc, contact-charge snapshot, budget undercharge, processCharge(), coupon fulfill, free trial decrement, category balance update.
94313-4328Update bid contact flags, contact time, paid subscription, interested, reason, requestor note.
104331-4351Create AutoQuoteContact when add_contact_activity=true; narrow path uses previous WantProviderQuote.param2 or narrow_match as param1.
114354-4362Update my-work unread count, then call chargeByCreditCard().
124364-4375Queue bid_contact_count, then commit transaction.
134377-4416Charge failures 101/102/104/105: rollback, retry-card may invalidate one card and retry; final failure may switch to new leads / narrow and throw public payment error.
144425-4427After transaction loop succeeds, call completeChargeByCreditCard().
154429-4449If request has life token, send Life55688 expert quoting event using latest transaction type 33/40 metadata.
164451-4465Notify provider only when non-narrow and non-direct-reserve; notification exception is logged and does not fail main flow.
174467-4472Write timing log and release payoff lock in finally.

Side Effects

AreaSide effects
lock / guardRedis payoff lock, basicAutoQuoteChecking(), blocked user / chat guards
feecontact-charge snapshot, getSiteFee(), budget undercharge, coupon fulfill, free trial decrement, category balance update
paymentprocessCharge(), wallet / card / TapPay path, transaction write, subscription log
bid statecontact flags, contact time, paid flag, interested flag, reason, requestor note
activity / countNarrowProviderAccept, AutoQuoteContact, my-work unread count, bid_contact_count
after commitcompleteChargeByCreditCard(), Life55688 expert quoting, provider notification, timing log
failurepayment logs, retry-card invalidation, final switch-to-new-leads / narrow fallback

Rollback Boundary

  • fee calculation, processCharge(), bid update, activity, unread count, chargeByCreditCard(), and bid_contact_count are inside the DB transaction.
  • completeChargeByCreditCard(), Life55688, provider notification, and timing log run after the transaction loop succeeds.
  • charge failure codes 101/102/104/105 rollback the transaction before retry / fallback handling.
  • RetryCreditCard can invalidate one auto quote card and retry.
  • final charge failure can switch to narrow/new leads when is_switch_fail=true and is_narrow_match=false.

Current Owner

FileSymbol
Lib/Model/QuoteBid.phpprocessWantToContactProvider at 683
Lib/Model/QuoteBid.phpapplyWantToContactContactChargeFeeSnapshot at 1271
Lib/Model/QuoteBid.phpapplyWantToContactBudgetLimit at 1328
Lib/Model/QuoteBid.phpprocessWantToContactCharge at 1400
Lib/Model/QuoteBid.phpchargeAutoQuoteDebtIfNeeded at 1616
Lib/Model/QuoteBid.phpchargeAutoQuoteDebtByPrimeIfNeeded at 1661
Lib/Model/QuoteBid.phpsendLife55688ExpertQuotingEvent at 1703
Lib/Model/StripeCustomer.phpchargeAllAutoQuoteDebt
Lib/Util/CreditCardUtil.phpchargeByAutoQuoteCustomer
Lib/Util/Life55688Util.phpaddEventExpertQuoting

Current Mapping Checklist

Legacy areaCurrent ownerVerification
option defaults and payoff lockQuoteBid::processWantToContactProviderdirect contact and special-flow tests acquire same mainline behavior
non-narrow service archived boundarybasicAutoQuoteChecking(..., $isNarrowMatch)testConsumerCanContactAutoQuoteProviderWhenServiceIsArchivedForNonNarrowLegacyPath
no subscription log fee recalculationresolveWantToContactSiteFee call in contact mainlinetestContactRecomputesSiteFeeInsteadOfUsingStaleBidTotalSiteFee
contact-charge fee snapshotapplyWantToContactContactChargeFeeSnapshottestContactChargeUsesQuoteBidSiteFeeSnapshot
weekly budget underchargeapplyWantToContactBudgetLimittestContactAutoQuoteUnderchargesWhenWeeklyBudgetRemainingIsLowerThanSiteFee
coupon fulfillfulfillWantToContactCoupontestProviderAcceptNarrowMatchFulfillsAvailableCoupon
free auto quote trialconsumeFreeAutoQuoteTrialIfNeededtestContactConsumesFreeAutoQuoteTrialWhenFeeReasonUsesTrial
wallet payment pathprocessWantToContactChargetestConsumerCanContactAutoQuoteProvider
existing type 40 must not skip contact chargesubscription log check plus new charge decisiontestExistingAutoQuoteSubmissionTransactionDoesNotSkipContactCharge
prime pay-by-prime successchargeAutoQuoteDebtByPrimeIfNeededtestConsumerCanContactProviderAndPayAutoQuoteDebtByPrimeWhenWalletInsufficient
prime foreign-card failure rollbackthrowForTapPayResult through contact APItestConsumerContactProviderPrimeForeignCardFailureHasNoContactSideEffects
retry-card single-card failurehandleRetryCreditCardFailure / fallbacktestAuthorizedModeFailsWhenCardChargeNeedsRetry
retry-card next-card successchargeAutoQuoteDebtIfNeeded looptestAuthorizedModeRetriesNextCardWhenRetryCreditCardFails
auto-refill debt chargeprocessWantToContactCharge plus StripeCustomer helpertestAuthorizedModeAutoRefillPaysOffDebtWhenPackageChargeSucceeds
narrow accept activity paramscreateNarrowProviderAcceptActivity and contact activity logictestProviderCanAcceptWaitingNarrowMatch
direct reserve notification boundaryis_direct_reserve option and direct reserve processortestCreatesDirectReserveArtifacts, testProviderAcceptDirectReserveNarrowMatchSendsDirectReserveConsumerEvent
direct user full-fee boundarycaller passes is_check_budget=false; contact mainline preserves full feetestCreatesDirectUserMatch
no payment method errorprocessWantToContactCharge no-card/no-prime guardtestAuthorizedModeFailsWhenProviderHasNoPaymentMethod, testProviderAcceptWithoutPrimeFailsWhenWalletAndCardAreUnavailable
blocked / disabled / blocked chat guardsassertWantToContactAllowedtestAuthorizedModeFailsWhenRequestorIsDisableMatchUser, testAuthorizedModeFailsWhenProviderAndRequestorAreBlockedInChat
Life55688 expert quotingsendLife55688ExpertQuotingEventcovered by current owner and detail doc; add fixture when life-token API test is needed

Verification

TestCoverage
tests/QuoteBidsWantToContactProviderTest.phpdirect contact provider API, fee/payment/activity/prime cases
tests/QuoteBidsProviderAcceptNarrowMatchTest.phpnarrow accept reuse of contact provider path
tests/SearchAddStep7IntegrationTest.phpsearch_add special flows that call this shared method

Last verified commands:

docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/QuoteBidsWantToContactProviderTest.php
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/QuoteBidsProviderAcceptNarrowMatchTest.php
docker exec -w /project-data cd63f9147e8d vendor/bin/phpunit tests/SearchAddStep7IntegrationTest.php

Result during this migration audit:

QuoteBidsWantToContactProviderTest: OK (11 tests, 175 assertions)
QuoteBidsProviderAcceptNarrowMatchTest: OK (7 tests, 113 assertions)
SearchAddStep7IntegrationTest: OK (20 tests, 285 assertions)

Remaining Gap

No current behavior gap is recorded for this rule.

Life55688 has current owner coverage and detail documentation, but a dedicated life-token fixture can still be added if future changes touch that integration. That would be additive coverage, not a known parity gap.

Notes

  • search_add authorized / direct reserve / direct user 只要呼叫這個 shared method,就必須回到本 rule 判斷完整 side effect。
  • helper-level payment internals 目前先由這條 rule 指向;若後續需要 SQL-level audit,再拆 child rule。