Skip to content

Indexer withdraw decoding, validated savings pagination, and 2 contract round-trip tests - #206

Merged
nayt9 merged 4 commits into
stowp:mainfrom
Kinkytech:fix/stow-assigned-batch
Aug 28, 2026
Merged

Indexer withdraw decoding, validated savings pagination, and 2 contract round-trip tests#206
nayt9 merged 4 commits into
stowp:mainfrom
Kinkytech:fix/stow-assigned-batch

Conversation

@Kinkytech

Copy link
Copy Markdown
Contributor

Summary

closes #65
closes #63
closes #35
closes #34

Changes

  • [Backend] — Indexer: decode withdraw event #65 — Indexer: decode withdraw event: SavingsProjectionService.apply() had a case 'deposit': handler crediting the flexible balance, but no case 'withdraw': at all — withdrawals silently fell through to the no-op default branch and were never projected off-chain. Added the handler, but deliberately not as a decrement-by-amount (debit) — the contract's withdraw event already carries its own post-withdrawal balance ((owner, amount, new_balance, now), see flexible.rs's withdraw()), so the new BalanceService.setBalance() sets the projected balance to that absolute value instead. This is what makes the projection idempotent: replaying the same event twice converges to the same final balance both times, whereas decrementing by the withdrawn amount would double-apply on redelivery. Mirrors the same idempotency approach LockedPlansService.upsertCreated already uses for locked_created.

  • [Backend] — Pagination + filtering helpers for savings lists #63 — Pagination + filtering helpers for savings lists: GET /savings/goals and GET /savings/locked accepted raw @Query('page')/@Query('limit') with no validation at all — Number(page) on arbitrary input could produce NaN, a negative skip, or silently-wrong results, never a rejection. While investigating I found backend/src/common/dto/pagination-query.dto.ts — a shared, already-tested PaginationQueryDto (page ≥1, limit 1-100, both rejected not clamped) that exists in the codebase but was never actually used anywhere. Built SavingsListQueryDto by extending that shared DTO rather than duplicating page/limit validation, adding only a sort field (asc/desc — direction only, not a free-text column name, so a caller can't request a sort column with no supporting index). Applied it to both endpoints via @Query() query: SavingsListQueryDto, each paired with @UsePipes(new ValidationPipe({ whitelist: true })) — this app has no global ValidationPipe (confirmed: none in main.ts or app.module.ts), so a DTO-typed @Query() param's decorators are otherwise inert; users.controller.ts already establishes this exact per-route workaround for the same reason. GoalsService.listByOwnerPaginated and LockedPlansService.listByOwner both gained an optional sort parameter controlling the direction of their existing fixed sort column.

  • [Contract] — Test: locked plan enforces unlock time #35 — Test: locked plan enforces unlock time: The individual pieces already existed as separate tests (locked_withdraw_before_unlock_rejected, locked_withdraw_exact_balance_after_unlock_succeeds), but no single test exercised the issue's exact scenario — create → early-reject → advance ledger time → withdraw → assert balances — as one continuous flow. Added locked_plan_enforces_unlock_time_round_trip covering exactly that, asserting the plan balance at each step (post-creation, post-rejected-early-withdrawal, post-successful-withdrawal).

  • [Contract] — Test: flexible deposit/withdraw round-trip #34 — Test: flexible deposit/withdraw round-trip: Similarly, flexible_withdraw_over_balance_rejected and flexible_withdraw_exact_balance_succeeds each isolated one scenario, and the confusingly-named existing flexible_deposit_withdraw test never actually calls withdraw() at all (despite its name — it only exercises deposit). Added flexible_deposit_withdraw_round_trip: fund → deposit → partial withdraw → assert balance → attempt over-withdraw against the remaining balance → assert rejected and balance unchanged → withdraw the rest → assert zero. The partial-withdraw-then-over-withdraw-against-remainder case wasn't covered by any existing test.

Test plan

  • Backend ([Backend] — Indexer: decode withdraw event #65, [Backend] — Pagination + filtering helpers for savings lists #63): npx jest on all 6 touched/new spec files — savings.controller.spec.ts, dto/pagination.dto.spec.ts (new), goals.service.spec.ts, locked-plans.service.spec.ts, balance.service.spec.ts, savings-projection.service.spec.ts64/64 passing, including 11 new DTO-validation tests (page/limit boundaries, sort accept/reject, combined multi-field errors) and new sort-direction tests for both list services (seeded with explicit fake-timer-controlled created_at/unlock_at values so ASC/DESC order is deterministic to assert against — the real-clock seed loop in the existing beforeEach runs fast enough that timestamps could collide within a millisecond, so those sort tests use their own isolated setup rather than the shared 25-goal seed). npx tsc --noEmit: zero errors in any touched file — confirmed via git stash that the one hit in balance.service.ts (TS1272, an emitDecoratorMetadata/isolatedModules interaction with a decorated constructor param) is pre-existing and unrelated, identical on unmodified code. npx eslint: 0 errors on every touched/new file after --fix (the only remaining warnings are pre-existing no-await on fake-repository test doubles I didn't create).
  • Contracts ([Contract] — Test: locked plan enforces unlock time #35, [Contract] — Test: flexible deposit/withdraw round-trip #34): Could not run cargo testsoroban-env-host's own testutils module fails to compile with a pre-existing ed25519-dalek/ChaCha20Rng/rand_core::CryptoRng trait-bound error (ChaCha20Rng doesn't implement DerefMut), unrelated to this repo's code and outside my control; reproduced identically with cargo check --tests and confirmed via a fresh clone of stowp/Stow main with zero changes applied — same error, same location. Since I couldn't compile, I verified as thoroughly as I could without it:
    • cargo fmt --check on test.rs specifically shows zero diffs — my additions match the crate's formatting exactly. (The crate as a whole has pre-existing formatting drift in admin.rs/flexible.rs/lib.rs/locked.rs/group_split.rs, confirmed byte-identical between this branch and a fresh clone of main via cargo fmt --check diff comparison — none of it is mine.)
    • Both new tests were built by directly copying the exact client-call signatures, error-wrapping pattern (Err(Ok(Error::...))), and env.ledger().set(LedgerInfo {...}) boilerplate from the immediately-adjacent, already-passing tests they combine (flexible_withdraw_over_balance_rejected/flexible_withdraw_exact_balance_succeeds for [Contract] — Test: flexible deposit/withdraw round-trip #34; locked_withdraw_before_unlock_rejected/locked_withdraw_exact_balance_after_unlock_succeeds for [Contract] — Test: locked plan enforces unlock time #35) — no new API surface, no new patterns, just recombined in one flow per each issue's literal requirement.
    • Please confirm CI (which likely runs in an environment without this dependency-resolution quirk) is green on the contract job.

@drips-wave

drips-wave Bot commented Aug 28, 2026

Copy link
Copy Markdown

@Kinkytech Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stow Ready Ready Preview Aug 28, 2026 3:20pm

@nayt9
nayt9 merged commit e8ccda6 into stowp:main Aug 28, 2026
2 checks 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

2 participants