Skip to content

fix(wallet): read the transaction direction set from one place - #197

Merged
argszero merged 1 commit into
mainfrom
fix/direction-set-single-source
Sep 13, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/direction-set-single-source

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

PR #196 introduced TX_INCOME_TYPES / TX_EXPENSE_TYPES and replaced the inline SQL literals in the summary / trend / dashboard aggregates with them, describing the constants as the single place to change. But GET /api/dashboard still decided each type's sign with an inline Rust match:

match m["type"].as_str().unwrap_or("") {
    "earn" | "topup" | "gift" => pts,
    _ => -pts,
}

That is a second copy of the same rule — and one that no SQL-grep finds. Changing the constant (say, deciding gift should be an expense) would move every SQL aggregate while leaving net unchanged, and nothing asserted the two agreed. Today the two agree by coincidence of spelling, not because anything enforces it.

This PR makes the claim true: both sets become arrays and every consumer reads them — SQL through sql_in_list(), Rust through contains().

// before (two copies)
const TX_INCOME_TYPES: &str = "'earn','topup','gift'";   // +5 inline `match` arms elsewhere
// after (one source, two renderings)
const TX_INCOME_TYPES: [&str; 3] = ["earn", "topup", "gift"];
// SQL:  IN ('earn','topup','gift')   ← sql_in_list(&TX_INCOME_TYPES)
// Rust: TX_INCOME_TYPES.contains(ty)

The generated SQL is byte-identical to the previous literals (asserted, see Tests), so no response changes.

Related Issue

None (no issue for this).

Changes

  • src/routes/wallet.rs — TX_INCOME_TYPES / TX_EXPENSE_TYPES change from SQL literals (&str) to arrays ([&str; 3]), and a new sql_in_list() renders them as the IN (…) value list.
  • src/routes/wallet.rs — signed_pts_expr(), summary and trend interpolate sql_in_list() instead of the literal (generated SQL is byte-identical ⇒ no response change).
  • src/routes/wallet.rs — dashboard's net folds with TX_INCOME_TYPES.contains(...); the inline match arm is gone.
  • src/routes/wallet.rs — the doc comments now state exactly what the module enforces (both consumers read the arrays) and no longer re-spell the type list in prose, where it could rot a third time.
  • 涉及配置/数据结构的改动已同步示例文件 — n/a (no config/schema change, no new i18n key).

Tests

  • cargo test — 187 passed, 0 failed (was 186; the new test is the only addition)

  • cargo fmt --check and cargo clippy --all-targets -- -D warnings — clean

  • New unit test dashboard_net_direction_holds_for_every_type — drives the real /api/dashboard handler with one row of each of the six types (earn 3.0, topup 2.0, gift 1.0, consume 10.0, expire 0.5, withdraw 0.25) and asserts net == −4.75 plus the per-type month rows, with the expectation hardcoded in the test rather than derived from the constants.

  • Byte-identity of the SQL — verified by assertion, not by reading: sql_in_list(&TX_INCOME_TYPES) == "'earn','topup','gift'", sql_in_list(&TX_EXPENSE_TYPES) == "'consume','expire','withdraw'", and signed_pts_expr("t") / signed_pts_expr("") equal the previous literals verbatim.

  • Proof the new test has teeth (count-neutral injection, per the project's rule that a gate must be shown to fail): add withdraw to the income array only —

    • fixed code → the new test goes red (net becomes −4.25, the assertion reads −4.75 → -4.25 at wallet.rs:917);
    • the pre-fix code with the same injection → the new test stays green (net does not read the array at all).

    That asymmetry is the evidence that the two sites were genuinely independent copies before, and that this PR is what joins them.

  • No behaviour change: the existing summary / trend / dashboard assertions (including dashboard_net_is_the_month_net_not_the_7_day_series and transactions_summary_and_dashboard_net_with_topup) pass untouched.

Checklist

  • 分支命名符合约定 (fix/)
  • Commit message 使用 Conventional Commits 格式
  • 单一职责,改动最小化

Not in scope

src/routes/ops.rs (month_in/month_out), src/gift.rs and ui/js/app.js (PTS_INCOME_TYPES) keep their own consistent copies — different modules/languages, swept by #196; the "one place" claim here is scoped to routes/wallet.rs, as the doc comment now says.

`dashboard` decided each type's sign with an inline
`match ty { "earn" | "topup" | "gift" => … }` — a second copy of the
income set that `signed_pts_expr` / `summary` / `trend` render from
`TX_INCOME_TYPES`. The SQL sites and the Rust site could therefore
drift apart, and editing the constant left `net` unchanged.

Both sets are now arrays and every consumer reads them: `sql_in_list`
renders the `IN (…)` value list (byte-identical to the previous
literals), and the `net` fold uses `contains`. A new test drives the
real handler over all six types with a hardcoded expectation.
@argszero
argszero merged commit 6997241 into main Sep 13, 2026
1 check passed
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