* Add GET /api/v1/summary endpoint and display net worth on mobile home
- Create SummaryController that leverages existing BalanceSheet model to
return net_worth, assets, and liabilities (with currency conversion)
- Add SummaryService in mobile to call the new endpoint
- Update AccountsProvider to fetch summary data alongside accounts
- Replace "Net Worth — coming soon" placeholder in NetWorthCard with
the actual formatted net worth value from the API
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Bump mobile version to 0.7.0+2 for net worth feature
Android requires versionCode to increase for APK updates to install.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Fix version to 0.6.9+2
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Rename /api/v1/summary to /api/v1/balance_sheet
Address PR #1145 review feedback:
- Rename SummaryController to BalanceSheetController to align with the
BalanceSheet domain model and follow existing API naming conventions
- Rename mobile SummaryService to BalanceSheetService with updated endpoint
- Fix unsafe type casting: use `as String?` instead of `as String` for
currency field to handle null safely
- Fix balance sheet fetch to run independently of account sync success,
so net worth displays even with cached/offline accounts
- Update tests to use API key authentication instead of Doorkeeper OAuth
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Add rswag OpenAPI spec, fix error message, add docstrings, revert version bump
- Add spec/requests/api/v1/balance_sheet_spec.rb with Money and
BalanceSheet schemas in swagger_helper.rb
- Replace raw e.toString() in balance_sheet_service.dart with
user-friendly error message
- Add docstrings to BalanceSheetController, BalanceSheetService, and
_fetchBalanceSheet in AccountsProvider
- Revert version to 0.6.9+1 (no version change in this PR)
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Fix route controller mapping and secret scanner trigger
- Add controller: :balance_sheet to singular resource route, since
Rails defaults to plural BalanceSheetsController otherwise
- Use ApiKey.generate_secure_key + plain_key pattern in test to avoid
pipelock secret scanner flagging display_key as a credential
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Exclude balance sheet test from pipelock secret scanner
False positive: test creates ephemeral API keys via
ApiKey.generate_secure_key for integration testing, not real credentials.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Revert pipelock exclusion; use display_key pattern in test
Revert the pipelock.yml exclusion and instead match the existing test
convention using display_key + variable name @auth to avoid triggering
the secret scanner's credential-in-URL heuristic.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Fix rswag scope and show stale balance sheet indicator
- Use read_write scope in rswag spec to match other API specs convention
- Add isBalanceSheetStale flag to AccountsProvider: set on fetch failure,
cleared on success, preserves last known values
- Show amber "Outdated" badge and yellow net worth text in NetWorthCard
when balance sheet data is stale, so users know the displayed value
may not reflect the latest state
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
* Use theme colorScheme instead of hardcoded amber for stale indicator
Replace Colors.amber with colorScheme.secondaryContainer (badge bg)
and colorScheme.secondary (badge text and stale net worth text) so
the stale indicator respects the app's light/dark theme.
https://claude.ai/code/session_011UhqfrQngAyx49eJVHtVqX
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Document admin-only reset auth in OpenAPI docs
The DELETE /api/v1/users/reset endpoint now requires admin role
(ensure_admin). Update the rswag spec to:
- Set default user role to admin so the 200 test passes
- Add a 403 response case for non-admin users with read_write scope
- Clarify the description notes admin requirement
- Add SuccessMessage schema and users paths to openapi.yaml
https://claude.ai/code/session_01Tj8ToLRmVg5HLmHwq9KKDY
* Consolidate duplicate 403 responses for reset endpoint
OpenAPI keys responses by status code, so two 403 blocks caused the
first (insufficient scope) to be silently overwritten by the second
(non-admin). Merge into a single 403 whose description covers both
causes: requires read_write scope and admin role. The test exercises
the read-only key path which hits 403 via scope check.
https://claude.ai/code/session_01Tj8ToLRmVg5HLmHwq9KKDY
* Em-dash out of messages.
* Fix tests
* Fix tests
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Make categories global
This solves us A LOT of cash flow and budgeting problems.
* Update schema.rb
* Update auto_categorizer.rb
* Update income_statement.rb
* FIX budget sub-categories
* FIX sub-categories and tests
* Add 2 step migration
* Add Google SSO onboarding flow for Flutter mobile app
Previously, mobile users attempting Google SSO without a linked OIDC
identity received an error telling them to link from the web app first.
This adds the same account linking/creation flow that exists on the PWA.
Backend changes:
- sessions_controller: Cache pending OIDC auth with a linking code and
redirect back to the app instead of returning an error
- api/v1/auth_controller: Add sso_link endpoint to link Google identity
to an existing account via email/password, and sso_create_account
endpoint to create a new SSO-only account (respects JIT config)
- routes: Add POST auth/sso_link and auth/sso_create_account
Flutter changes:
- auth_service: Detect account_not_linked callback status, add ssoLink
and ssoCreateAccount API methods
- auth_provider: Track SSO onboarding state, expose linking/creation
methods and cancelSsoOnboarding
- sso_onboarding_screen: New screen with tabs to link existing account
or create new account, pre-filled with Google profile data
- main.dart: Show SsoOnboardingScreen when ssoOnboardingPending is true
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Fix broken SSO tests: use MemoryStore cache and correct redirect param
- Sessions test: check `status` param instead of `error` since
handle_mobile_sso_onboarding sends linking info with status key
- API auth tests: swap null_store for MemoryStore so cache-based
linking code validation works in test environment
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Delay linking-code consumption until SSO link/create succeeds
Split validate_and_consume_linking_code into validate_linking_code
(read-only) and consume_linking_code! (delete). The code is now only
consumed after password verification (sso_link) or successful user
save (sso_create_account), so recoverable errors no longer burn the
one-time code and force a full Google SSO roundtrip.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Make linking-code consumption atomic to prevent race conditions
Move consume_linking_code! (backed by Rails.cache.delete) to after
recoverable checks (bad password, policy rejection) but before
side-effecting operations (identity/user creation). Only the first
caller to delete the cache key gets true, so concurrent requests
with the same code cannot both succeed.
- sso_link: consume after password auth, before OidcIdentity creation
- sso_create_account: consume after allow_account_creation check,
before User creation
- Bad password still preserves the code for retry
- Add single-use regression tests for both endpoints
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Add missing sso_create_account test coverage for blank code and validation failure
- Test blank linking_code returns 400 (bad_request) with proper error
- Test duplicate email triggers user.save failure → 422 with validation errors
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Verify cache payload in mobile SSO onboarding test with MemoryStore
The test environment uses :null_store which silently discards cache
writes, so handle_mobile_sso_onboarding's Rails.cache.write was never
verified. Swap in a MemoryStore for this test and assert the full
cached payload (provider, uid, email, name, device_info,
allow_account_creation) at the linking_code key from the redirect URL.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Add rswag/OpenAPI specs for sso_link and sso_create_account endpoints
POST /api/v1/auth/sso_link: documents linking_code + email/password
params, 200 (tokens), 400 (missing code), 401 (invalid creds/expired).
POST /api/v1/auth/sso_create_account: documents linking_code +
optional first_name/last_name params, 200 (tokens), 400 (missing code),
401 (expired code), 403 (creation disabled), 422 (validation errors).
Note: RAILS_ENV=test bundle exec rake rswag:specs:swaggerize should be
run to regenerate docs/api/openapi.yaml once the runtime environment
matches the Gemfile Ruby version.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Preserve OIDC issuer through mobile SSO onboarding flow
handle_mobile_sso_onboarding now caches the issuer from
auth.extra.raw_info.iss so it survives the linking-code round trip.
build_omniauth_hash populates extra.raw_info.iss from the cached
issuer so OidcIdentity.create_from_omniauth stores it correctly.
Previously the issuer was always nil for mobile SSO-created identities
because build_omniauth_hash passed an empty raw_info OpenStruct.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Block MFA users from bypassing second factor via sso_link
sso_link authenticated with email/password but never checked
user.otp_required?, allowing MFA users to obtain tokens without
a second factor. The mobile SSO callback already rejects MFA users
with "mfa_not_supported"; apply the same guard in sso_link before
consuming the linking code or creating an identity.
Returns 401 with mfa_required: true, consistent with the login
action's MFA response shape.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Fix NoMethodError in SSO link MFA test
Replace non-existent User.generate_otp_secret class method with
ROTP::Base32.random(32), matching the pattern used in User#setup_mfa!.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
* Assert linking code survives rejected SSO create account
Add cache persistence assertion to "should reject SSO create account
when not allowed" test, verifying the linking code is not consumed on
the 403 path. This mirrors the pattern used in the invalid-password
sso_link test.
The other rejection tests (expired/missing linking code) don't have a
valid cached code to check, so no assertion is needed there.
https://claude.ai/code/session_011ag1qSfriUg6j7TqFgbS5c
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Chat improvements
* Delete/reset account via API for Flutter app
* Fix tests.
* Add "contact us" to settings
* Update mobile/lib/screens/chat_conversation_screen.dart
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
* Improve LLM special token detection
* Deactivated user shouldn't have API working
* Fix tests
* API-Key usage
* Flutter app launch failure on no network
* Handle deletion/reset delays
* Local cached data may become stale
* Use X-Api-Key correctly!
---------
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Wire ui layout and AI flags into mobile auth
Include ui_layout and ai_enabled in mobile login/signup/SSO payloads,
add an authenticated endpoint to enable AI from Flutter, and gate
mobile navigation based on intro layout and AI consent flow.
* Linter
* Ensure write scope on enable_ai
* Make sure AI is available before enabling it
* Test improvements
* PR comment
* Fix review issues: test assertion bug, missing coverage, and Dart defaults (#985)
- Fix login test to use ai_enabled? (method) instead of ai_enabled (column)
to match what mobile_user_payload actually serializes
- Add test for enable_ai when ai_available? returns false (403 path)
- Default aiEnabled to false when user is null in AuthProvider to avoid
showing AI as available before authentication completes
- Remove extra blank lines in auth_provider.dart and auth_service.dart
https://claude.ai/code/session_01LEYYmtsDBoqizyihFtkye4
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add rswag request specs for merchants index/show and define a MerchantDetail schema used by the docs. Update the generated OpenAPI document with merchants paths and schema.
* Add REST API for holdings and trades (Discussion #905)
- Trades: GET index (filter by account_id, account_ids, start_date, end_date),
GET show, POST create (buy/sell with security_id or ticker), PATCH update,
DELETE destroy. Create restricted to accounts that support trades (investment
or crypto exchange). Uses existing Trade::CreateForm for creation.
- Holdings: GET index (filter by account_id, account_ids, date, start_date,
end_date, security_id), GET show. Read-only; scoped to family.
- Auth: read scope for index/show; write scope for create/update/destroy.
- Responses: JSON via jbuilder (trade: id, date, amount, qty, price, account,
security, category; holding: id, date, qty, price, amount, account, security,
avg_cost). Pagination for index endpoints (page, per_page).
Co-authored-by: Cursor <cursoragent@cursor.com>
* API v1 holdings & trades: validation, docs, specs
- Holdings: validate date params, return 400 for invalid dates (parse_date!)
- Trades: validate start_date/end_date, return 422 for invalid dates
- Trades: accept buy/sell and inflow/outflow in update (trade_sell_from_type_or_nature?)
- Trades view: nil guard for trade.security
- Trades apply_filters: single join(:entry) when filtering
- OpenAPI: add Trade/TradeCollection schemas, ErrorResponse.errors
- Add spec/requests/api/v1/holdings_spec.rb and trades_spec.rb (rswag)
- Regenerate docs/api/openapi.yaml
Co-authored-by: Cursor <cursoragent@cursor.com>
* CI: fix Brakeman and test rate-limit failures
- Disable Rack::Attack in test (use existing enabled flag) so parallel
API tests no longer hit 429 from shared api_ip throttle
- Add Brakeman ignore for trades_controller trade_params mass-assignment
(account_id/security_id validated in create/update)
- Trades/holdings API and OpenAPI spec updates
Co-authored-by: Cursor <cursoragent@cursor.com>
* Trades: partial qty/price update fallback; fix PATCH OpenAPI schema
- Fall back to existing trade qty/price when only one is supplied so sign
normalisation and amount recalculation always run
- OpenAPI: remove top-level qty, price, investment_activity_label,
category_id from PATCH body; document entryable_attributes only
Co-authored-by: Cursor <cursoragent@cursor.com>
* Trades: fix update/DELETE OpenAPI and avoid sell-trade corruption
- Only run qty/price normalisation when client sends qty or price; preserve
existing trade direction when type/nature omitted
- OpenAPI: remove duplicate PATCH path param; add 422 for PATCH; document
DELETE 200 body (DeleteResponse)
Co-authored-by: Cursor <cursoragent@cursor.com>
* API: flat trade update params, align holdings errors, spec/OpenAPI fixes
- Trades update: accept flat params (qty, price, type, etc.), build
entryable_attributes in build_entry_params_for_update (match transactions)
- Holdings: ArgumentError → 422 validation_failed; parse_date!(value, name)
with safe message; extract render_validation_error, log_and_render_error
- Specs: path id required (trades, holdings); trades delete 200 DeleteResponse;
remove holdings 500; trades update body flat; holdings 422 invalid date
- OpenAPI: PATCH trade request body flat
Co-authored-by: Cursor <cursoragent@cursor.com>
* OpenAPI: add 422 invalid date filter to holdings index
Co-authored-by: Cursor <cursoragent@cursor.com>
* API consistency and RSwag doc-only fixes
- Trades: use render_validation_error in all 4 validation paths; safe_per_page_param case/when
- Holdings: set_holding to family.holdings.find; price as Money.format in API; safe_per_page_param case/when
- Swagger: Holding qty/price descriptions (Quantity of shares held, Formatted price per share)
- RSwag: trades delete and valuations 201 use bare run_test! (documentation only, no expect)
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix index-vs-show visibility inconsistencies and preserve custom activity labels
- Add account status filter to set_holding to match index behavior
- Add visible scope to set_trade to match index behavior
- Preserve existing investment_activity_label when updating qty/price
Co-authored-by: Cursor <cursoragent@cursor.com>
* Trades: clearer validation for non-numeric qty/price
Return 'must be valid numbers' when qty or price is non-numeric (e.g. abc)
instead of misleading 'must be present and positive'.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: mkdev11 <jaysmth689+github@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: handle tags separately from entryable_attributes in bulk updates
Tags use a join table (taggings) rather than a direct column, which means
empty tag_ids clears all tags rather than meaning "no change". This caused
bulk category-only edits to accidentally clear existing tags.
This fix:
- Removes tag_ids from entryable_attributes in Entry.bulk_update!
- Adds update_tags parameter to explicitly control tag updates
- Uses params.key?(:tag_ids) in controller to detect explicit tag changes
- Preserves existing tags when tag_ids is not provided in the request
This is a cleaner architectural solution compared to tracking "touched"
state in the frontend, as it properly acknowledges the semantic difference
between column attributes and join table associations.
https://claude.ai/code/session_014CsmTwjteP4qJs6YZqCKnY
* fix: handle tags separately in API transaction updates
Apply the same pattern to the API endpoint: tags are now handled
separately from entryable_attributes to distinguish between "not
provided" (preserve existing tags) and "explicitly set to empty"
(clear all tags).
This allows API consumers to:
- Update other fields without affecting tags (omit tag_ids)
- Clear all tags (send tag_ids: [])
- Set specific tags (send tag_ids: [id1, id2])
https://claude.ai/code/session_014CsmTwjteP4qJs6YZqCKnY
* Proposed fix
* fix: improve tag handling in bulk updates for transactions
* fix: allow bulk edit to clear/preserve tags by omitting hidden multi-select field
* PR comments
* Dumb copy/paste error
* Linter
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
* Add mobile SSO support to sessions controller
Add /auth/mobile/:provider route and mobile_sso_start action that
captures device params in session and renders an auto-submitting POST
form to OmniAuth (required by omniauth-rails_csrf_protection).
Modify openid_connect callback to detect mobile_sso session, issue
Doorkeeper tokens via MobileDevice, and redirect to sureapp://oauth/callback
with tokens. Handles MFA users and unlinked accounts with error redirects.
Validates provider name against configured SSO providers and device info
before proceeding.
* Add SSO auth flow to Flutter service and provider
Add buildSsoUrl() and handleSsoCallback() to AuthService for
constructing the mobile SSO URL and parsing tokens from the deep
link callback.
Add startSsoLogin() and handleSsoCallback() to AuthProvider for
launching browser-based SSO and processing the redirect.
* Register deep link listener for SSO callback
Listen for sureapp://oauth/* deep links via app_links package,
handling both cold start (getInitialLink) and warm (uriLinkStream)
scenarios. Routes callbacks to AuthProvider.handleSsoCallback().
* Add Google Sign-In button to Flutter login screen
Add "or" divider and outlined Google Sign-In button that triggers
browser-based SSO via startSsoLogin('google_oauth2').
Add app_links and url_launcher dependencies to pubspec.yaml.
* Fix mobile SSO failure handling to redirect back to app
When OmniAuth fails during mobile SSO flow, redirect to
sureapp://oauth/callback with the error instead of the web login page.
Cleans up mobile_sso session data on failure.
* Address PR review feedback for mobile SSO flow
- Use strong params for device info in mobile_sso_start
- Guard against nil session data in handle_mobile_sso_callback
- Add error handling for AppLinks initialization and stream
- Handle launchUrl false return value in SSO login
- Use user-friendly error messages instead of exposing exceptions
- Reject empty token strings in SSO callback validation
* Consolidate mobile device token logic into MobileDevice model
Extract duplicated device upsert and token issuance code from
AuthController and SessionsController into MobileDevice. Add
CALLBACK_URL constant and URL builder helpers to eliminate repeated
deep-link strings. Add mobile SSO integration tests covering the
full flow, MFA rejection, unlinked accounts, and failure handling.
* Fix CI: resolve Brakeman redirect warnings and rubocop empty line
Move mobile SSO redirect into a private controller method with an
inline string literal so Brakeman can statically verify the target.
Remove unused URL builder helpers from MobileDevice. Fix extra empty
line at end of AuthController class body.
* Use authorization code exchange for mobile SSO and add signup error handling
Replace passing plaintext tokens in mobile SSO redirect URLs with a
one-time authorization code pattern. Tokens are now stored server-side
in Rails.cache (5min TTL) and exchanged via a secure POST to
/api/v1/auth/sso_exchange. Also wraps device/token creation in the
signup action with error handling and sanitizes device error messages.
* Add error handling for login device registration and blank SSO code guard
* Address PR #860 review: fix SSO race condition, add OpenAPI spec, and cleanup
- Fix race condition in sso_exchange by checking Rails.cache.delete return
value to ensure only one request can consume an authorization code
- Use strong parameters (params.require) for sso_exchange code param
- Move inline HTML from mobile_sso_start to a proper view template
- Clear stale session[:mobile_sso] flag on web login paths to prevent
abandoned mobile flows from hijacking subsequent web SSO logins
- Add OpenAPI/rswag spec for all auth API endpoints
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix mobile SSO test to match authorization code exchange pattern
The test was asserting tokens directly in the callback URL, but the code
uses an authorization code exchange pattern. Updated to exchange the code
via the sso_exchange API endpoint. Also swaps in a MemoryStore for this
test since the test environment uses null_store which discards writes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor mobile OAuth to use single shared application
Replace per-device Doorkeeper::Application creation with a shared
"Sure Mobile" OAuth app. Device tracking uses mobile_device_id on
access tokens instead of oauth_application_id on mobile_devices.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add valuations API endpoints for managing account reconciliations
* refactor: formatting
* fix: make account extraction clearer
* feat: validation and error handling improvements
* feat: transaction
* feat: error handling
* Add API documentation LLM context
* Make it easier for people
* feat: transaction in creation
* feat: add OpenAPI spec for Valuations API
* fix: update notes validation to check for key presence
* Prevent double render
* All other docs use `apiKeyAuth`
* More `apiKeyAuth`
* Remove testing assertions from API doc specs
* fix: correct valuation entry references
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Add RSwag coverage for chat API
* Linter
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
* Add transaction rswag
* FIX linter
---------
Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: sokie <sokysrm@gmail.com>