# Payment amount cleanup and Matching countdown — 2026-10-05

## Completed changes

- Removed `TRAVELIN_ALLOW_PAYMENT_TEST_OVERRIDE` and `TRAVELIN_PAYMENT_TEST_AMOUNT` assignments and their obsolete comment from the private local configuration. No credential value is recorded here.
- Removed both variables from `config/local_secrets.example.bat`.
- Deleted `server/payment_amount_policy.dart` and `test/server/payment_amount_policy_test.dart`, which exclusively implemented/tested the retired override. Both files were read in full before deletion; their former contents are available in this turn's tool history. This workspace has no Git working tree, and no dedicated rollback copy of these two files was created.
- Removed the policy import, startup log state, selector, verification override exception, and test-amount logging from `server/gemini_proxy.dart`.
- New Payment amount is the validated winning bid price. Existing rows keep their stored amount. The same amount is passed to payment instructions/PromptPay QR and stored in the Payment row. Verification reads the stored amount, requires equality with the bid price for an unverified row, and sends that expected amount to the existing verifier.
- Existing `paid` rows still return before any provider verification. Persisted verification metadata, `verificationSucceeded`, and Flutter's `ผลตรวจสอบเดิมจาก SlipOK` display are retained.
- Evidence-hash and transaction-reference duplicate checks and their SQL unique indexes are unchanged. SlipOK receiver/amount validation and rejection codes 1012/1013/1014 are unchanged.

## Countdown root cause and implementation

`MatchingStatusView` previously rendered the last `waitingRemainingSeconds` value in a StatelessWidget. `GroupsPage` refreshed Backend status once per minute, so formatting alone could not make the displayed number change each second.

The existing view now contains a small Stateful `_WaitingCountdown` in `lib/widgets/matching_status_view.dart`:

- Prefer Backend `waitingDeadlineAt`. When `serverNow` exists, anchor the server deadline interval to local receipt time to compensate for device/server clock skew.
- If no deadline exists, anchor a local deadline once from the response's remaining seconds.
- A single one-second timer recomputes the difference from the deadline; it never decrements a counter cumulatively. Remaining time is rounded up to the next second and clamped at zero.
- The timer is cancelled/replaced when a waiting window/source changes and cancelled at zero, in background/inactive states, or on dispose.
- On resume, recompute from current time and restart only if time remains.
- At zero, invoke the existing `onRefresh` callback once per waiting window, after a frame and only when the view is not busy. `GroupsPage._refreshStatus` continues to use `MatchingViewModel.load` -> existing `/api/matching/status` API. Each visual tick does not make a network request.
- Timeout/fallback/matching decisions remain Backend responsibilities. A Backend response with alternatives still switches to the existing alternatives UI.

## Automated verification

1. `flutter test test/widgets/matching_status_view_test.dart`: **15 PASS**.
   Covers 60->59->58->57, 61/3601 formatting, zero clamping, expiry refresh once, dispose, renewed deadline/preference, normal parent rebuilds, clock skew, background time jump, and busy-state deferral. Timers are explicitly disposed and Flutter's pending-timer invariants passed.
2. Combined targeted run: **118 PASS**:

   ```powershell
   flutter test test/services/matching_api_service_test.dart test/server/slipok_configuration_test.dart test/server/promptpay_qr_test.dart test/server/slipok_verifier_test.dart test/server/slip_verification_service_test.dart test/server/payment_verification_state_test.dart test/group_service_test.dart test/payment_page_test.dart test/widget_test.dart
   ```

3. `flutter analyze`: **PASS, No issues found**.
4. `dart analyze server/gemini_proxy.dart lib/widgets/matching_status_view.dart test/widgets/matching_status_view_test.dart test/server/slipok_configuration_test.dart`: **PASS, No issues found**.
5. Read-only configuration/runtime-source checks confirm the retired assignments are absent, the policy file is absent, and runtime source no longer reads either override variable.

Total: **133 automated tests passed**. These are unit, mock-HTTP, source-contract, and Flutter widget tests. No live database/API/Emulator E2E claim is made.

## Activation and remaining checks

- Restart only the Backend process on 8787 to load the cleaned code/configuration. Old environment assignments inherited by a console cannot activate the removed feature because runtime no longer reads them.
- Hot Restart Flutter, or rebuild/relaunch its current development build, then reopen the existing Waiting page to see the one-second countdown. Do not submit Preference again for this purpose.
- No MariaDB restart, schema migration, or database edit is required.
- Historical paid 1-baht test Payments retain their original amount and saved verification. Any historical unpaid test Payment whose stored amount differs from the bid is rejected by the restored amount check; it is not silently repriced or repaired.
- Emulator verification of the newly built UI has not been performed this round. Use the existing Preference/Waiting record for that check.

## Data and provider safety

No database connection or data mutation was performed during development/testing. No new Payment, transaction, evidence upload, or SlipOK request was made. No MariaDB service/datadir/recovery backup was touched. No user/group records (#115/#102 included) were accessed or changed.
