Skip to content

#688 feat(api): link savings goal to a specific vault — add vault_id to savings_goals#748

Open
Adeolu01 wants to merge 4 commits into
Suncrest-Labs:mainfrom
Adeolu01:feat/688-link-savings-goal-to-vault
Open

#688 feat(api): link savings goal to a specific vault — add vault_id to savings_goals#748
Adeolu01 wants to merge 4 commits into
Suncrest-Labs:mainfrom
Adeolu01:feat/688-link-savings-goal-to-vault

Conversation

@Adeolu01

Copy link
Copy Markdown
Contributor

Summary

SavingsGoalService.enrichProgress computed current_amount via SumVaultBalance(userID, currency), summing every vault a user holds in that currency. For a user with multiple vaults, every goal in the same currency reported the combined balance of all vaults — an inflated, incorrect figure. This PR links a goal to a specific vault and scopes current_amount to that vault.

closes #688

Changes

Schemamigrations/040_add_savings_goal_vault_id.{up,down}.sql

  • Adds nullable vault_id UUID REFERENCES vaults(id) ON DELETE SET NULL plus a supporting index.
  • NULL is allowed for backward compatibility: goals created before this migration keep working via the legacy fallback.

Domaininternal/domain/savingsgoal/model.go

  • SavingsGoal gains VaultID *uuid.UUID (json:"vault_id,omitempty").

Serviceinternal/service/savings_goal_service.go

  • CreateSavingsGoalInput accepts an optional VaultID.
  • On create, when a vault is supplied it is validated: it must exist, belong to the authenticated user, and share the goal's currency.
  • enrichProgress now reads the linked vault's CurrentBalance. Goals with no vault_id (and goals whose linked vault has since been soft-deleted) fall back to the original sum-all logic, so there is no regression for existing data.
  • The service depends on a narrow VaultReader interface (GetVault only) rather than the full vault repository — interface segregation keeps the dependency minimal and the unit tests light.

Handlerinternal/handler/savings_goal_handler.go

  • Parses an optional vault_id from the create request (malformed UUID → 400).
  • Maps an ownership violation to 403 FORBIDDEN; a currency mismatch surfaces as 400 VALIDATION_ERROR.

Wiringcmd/api/main.go injects the existing vaultRepository into the savings goal service.

Validation behaviour

Case Result
Valid vault_id owned by caller, matching currency linked; current_amount = that vault's balance
vault_id belonging to another user 403 FORBIDDEN
Vault currency ≠ goal currency (e.g. XLM vault on USDC goal) 400vault currency (XLM) does not match goal currency (USDC)
vault_id not found 400 VALIDATION_ERROR
No vault_id (legacy) falls back to sum-all-vaults

Tests

  • Service unit tests: links a vault and reflects its balance; foreign vault → unauthorized; currency mismatch; vault not found; per-vault isolation (two vaults + two goals each report only their own balance, never the sum); unlinked goal falls back to sum-all.
  • Handler tests: vault link echoed on create; foreign vault → 403; malformed vault_id400.
  • Integration tests (guarded by TEST_DATABASE_DSN): two vaults / two goals isolation through the real Postgres path with a round-trip re-read; foreign-vault rejection.

Acceptance criteria

  • Migration adds nullable vault_id column to savings_goals
  • Creating a goal with a valid vault_id links it correctly
  • A vault_id belonging to another user returns 403
  • current_amount reflects only the linked vault's balance
  • Goals without vault_id fall back to sum-all-vaults (no regression)
  • Vault currency mismatch returns 400
  • Integration test: two vaults, two goals, each goal reflects its own vault's balance

…scope current_amount to the linked vault

Add a nullable vault_id to savings_goals so a goal's current_amount reflects
only the balance of its linked vault instead of summing every vault in the
currency. Goals without a vault_id keep the legacy sum-all fallback, so
existing goals are unaffected.

- migration 040 adds nullable vault_id UUID REFERENCES vaults(id) ON DELETE SET NULL
- CreateSavingsGoalInput accepts vault_id; the service validates the vault
  exists, belongs to the caller (403), and shares the goal currency (400)
- enrichProgress reads the linked vault balance, falling back to sum-all for
  unlinked or soft-deleted vaults
- service depends on a narrow VaultReader interface (GetVault only)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Adeolu01 Adeolu01 requested a review from 0xDeon as a code owner June 26, 2026 03:17
@drips-wave

drips-wave Bot commented Jun 26, 2026

Copy link
Copy Markdown

@Adeolu01 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

Adeolu01 and others added 3 commits June 26, 2026 04:57
main on this branch did not compile (pre-existing bad-merge artifacts that
blocked go build, govulncheck and gosec). Repaired the minimum needed:

- cmd/api: a duplicate notificationDispatcher := left the websocket channel
  orphaned and triggered "no new variables on left side of :="; merged the
  websocket and push channels into the single dispatcher used by savings goals
  and the recurring-deposit scheduler
- ws.Hub: add PushToUser so it satisfies notifications.WebSocketHub (the
  websocket notification channel adapter)
- savings schedule service: depend on the narrow VaultReader interface instead
  of the full vault.Repository (it only calls GetVault), unblocking the service
  test package fakes
- savings schedule test fake: implement the goal repo's UpdateMilestones method

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recurring-deposit integration test lives in package postgres and imports
service, while service (admin_rebalance_service.go) imported postgres for the
ErrRebalanceInFlight sentinel — a cycle that broke 'go test' once the build
compiled. Move the sentinel to the admin domain package (imported by both),
re-export it from postgres as an alias for existing callers, and drop the
service->postgres import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n test

Pre-existing unused import that was masked by the earlier import cycle; it broke
go test and govulncheck package loading once the cycle was resolved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@0xDeon 0xDeon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge conflict with main. Rebase onto main and resolve conflicts before this can be merged.

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.

feat(api): link savings goal to a specific vault — add vault_id to savings_goals table

2 participants