Skip to content

fix(wallet): accept the same transaction type filter on the list and trend endpoints - #195

Merged
argszero merged 1 commit into
mainfrom
fix/trend-type-filter
Sep 13, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/trend-type-filter

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

GET /api/transactions/trend answered 400 to ?type=expire while GET /api/transactions accepted it, so choosing 赠送过期 in the transactions type filter left the list correct but replaced the trend chart with "trend data failed to load".

The set of accepted values for the type query parameter was written twice — one whitelist per handler. The change that introduced the expire type (#194) updated the list arm, the list error message, the trend SQL and the TxTrendQuery doc comment, but not the trend validator, which kept the old five-value whitelist. That is user-visible: the frontend sends the same type value to both endpoints (ui/js/app.js, loadTransactions) and swallows a failed trend request into trend = null, which renderTxTrend renders as "trend data failed to load".

This PR removes the duplication rather than adding expire to the second copy: one TX_FILTER_TYPES constant and one parse_tx_type helper are now shared by both handlers, and the 400 message is derived from the same array, so the two endpoints can no longer drift apart.

Runtime evidence before the fix (throwaway probe, print-only, reverted afterwards — file md5 restored):

GET /api/transactions?type=expire                    -> 200 OK
  body: {..."summary":{"expense_pts":1.0,"income_pts":0.0,"net_pts":-1.0},"total":1...}
GET /api/transactions/trend?type=expire&bucket=day   -> 400 Bad Request
  body: {"error":"type 必须为 consume / earn / topup / gift / withdraw / all"}

Severity — display, not settlement. The ledger rows, the list endpoint and the trend SQL all treat expire as an expense already; what broke was one UI panel for one filter value.

Related Issue

No issue exists for this (the work came from an internal recon cycle); none is fabricated. closingIssuesReferences is intentionally empty.

Changes

  • src/routes/wallet.rs: added TX_FILTER_TYPES (consume | earn | topup | gift | expire | withdraw) as the single source of truth for the type filter, plus parse_tx_type(raw) (""/"all" → None; a member → Some(member); anything else → 400).
  • Both handlers (transactions and transactions_trend) now call that one helper; the 400 message is built from the same array (format!("type 必须为 {} / all", TX_FILTER_TYPES.join(" / "))). The trend endpoint therefore accepts expire, and its 400 message now names the full shared list instead of the stale five-value one.
  • The two query-struct docs now point at TX_FILTER_TYPES instead of restating the list (a restated list is what silently disagreed with the trend validator).
  • Doc-only correction in the same function: transactions_trend's prose still said the 口径 was "consume 为负", which stopped being true when the direction was made type-based (#191/#194); it now says direction comes from type, not from the sign of pts.
  • Not touched: the direction SQL sets (income IN ('earn','topup','gift') / expense IN ('consume','expire','withdraw')) — a different concept, deliberately written per query. withdraw stays accepted although it has no writer, because the filter set is what the UI offers, not what exists yet.
  • No ui/ change, no new i18n key, no cache-bust bump, no configuration / data-structure change.

Tests

  • cargo test 全部通过 (185 passed, baseline 184 + 1 new)

  • cargo fmt --check 通过

  • cargo clippy --all-targets -- -D warnings 通过

  • 新增/更新了单元测试:

    every_accepted_type_filter_value_is_accepted_by_both_transaction_endpoints — asserts the specification (every filter value the UI offers must be accepted by every endpoint that takes a type):

    1. each member of TX_FILTER_TYPES → 200 on both /api/transactions and /api/transactions/trend;
    2. all and the empty default → 200 on both;
    3. positive control: a value outside the set (refund) → 400 on both, and the message names every member of the shared list.

    Because the assertions iterate the single source of truth, adding a type to only one endpoint now fails the suite.

  • A/B: reverting only the trend handler's parse_tx_type call back to its old hand-written whitelist ⇒ exactly 1 red / 184 green, the red being this test failing on expire at /api/transactions/trend:
    assertion left == right failed: \expire` 应被两个端点接受(/api/transactions/trend?type=expire&bucket=day): {"error":"type 必须为 consume / earn / topup / gift / withdraw / all"}`

Checklist

  • 分支命名符合约定(fix/)
  • Commit message 使用 Conventional Commits 格式
  • 单一职责,改动最小化(1 个文件,无 ui/、无 i18n 键、无 schema 变更)

`GET /api/transactions/trend` answered 400 to `?type=expire` while
`GET /api/transactions` accepted it. The accepted-value set for the `type`
parameter was written twice — one whitelist per handler. The previous change
that introduced the `expire` type updated the list arm, the list error
message, the trend SQL and the `TxTrendQuery` doc comment, but not the trend
validator, which kept the old five-value whitelist.

The frontend sends the *same* `type` value to both endpoints and swallows a
failed trend request into `trend = null`, so selecting 赠送过期 in the type
filter showed a correct list next to a chart reading "trend data failed to
load".

Remove the duplication instead of adding `expire` to the second copy: one
`TX_FILTER_TYPES` constant plus one `parse_tx_type` helper are now shared by
both handlers, and the 400 message is derived from that same array, so the
two endpoints cannot drift apart again. Add a parity test that drives both
endpoints with every member of the set (plus `all` / empty, and a bogus value
as the positive control).
@argszero
argszero merged commit f55cfd2 into main Sep 13, 2026
1 check passed
@argszero
argszero deleted the fix/trend-type-filter branch September 13, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant