Account Statement Management#
Account statement management in Sure covers uploading, organizing, linking, and reviewing financial statement files (PDF, CSV, XLSX) at both the family level (Statement Vault) and per-account level. The central controller is AccountStatementsController, with supporting UI embedded in the account detail page via lazy-loaded Turbo Frames.
Related articles (separate concerns):
- Statement Deduplication — SHA-256/MD5 hashing and coverage-level duplicate detection
- Statement Period Extraction —
MetadataDetectorauto-fill ofperiod_start_on/period_end_on- Account Statement Reconciliation — three-point balance checks and monthly coverage grid
- Turbo Frame Navigation —
data-turbo-frame="_top"fix for statement links inside the account tab frame
Controller Actions#
AccountStatementsController provides 8 public actions. Two before_action guards apply across the surface :
set_statement— scopes the lookup to the current family and enforcesviewable_by?(Current.user), raisingRecordNotFoundfor unauthorized access.ensure_statement_manager!— redirects non-managers away fromindex,create,update,destroy,link,unlink, andreject.
| Action | Method/Path | Behavior |
|---|---|---|
index | GET /account_statements | Lists unmatched and linked statements in separate paginated scopes |
show | GET /account_statements/:id | Displays file metadata, editable fields, reconciliation checks |
create | POST /account_statements | Bulk file upload loop; collects created/duplicates/errors per file |
update | PATCH /account_statements/:id | Updates metadata fields; optionally re-links to a new account |
link | PATCH /account_statements/:id/link | Links to a specified or suggested account |
unlink | PATCH /account_statements/:id/unlink | Removes account association, returns to unmatched status |
reject | PATCH /account_statements/:id/reject | Clears suggested_account, sets rejected status |
destroy | DELETE /account_statements/:id | Destroys the statement; redirects to account statements tab or vault |
Both index and show render with layout: "settings" , which means they have no matching Turbo Frame for the account tab — links navigating to these pages from within the account detail frame require data-turbo-frame="_top" (see Turbo Frame Navigation).
File Upload Workflow#
create accepts a files array and iterates over each file independently :
AccountStatement.prepare_upload!(file)— reads bytes, computes SHA-256 (content_sha256) and MD5 (checksum), validates extension/content-type/size, and stores results in a prepared-upload struct.AccountStatement.create_from_prepared_upload!— attaches the file, runsMetadataDetector, callsassign_account_match, and persists the record.
Per-file errors are collected without aborting the loop:
InvalidUploadError→ invalid file type flashDuplicateUploadError→ duplicate count flashActiveRecord::RecordInvalid→ inline error message flash
Redirect after create is determined by redirect_after_create:
- Account specified →
account_path(account, tab: "statements") - No account but a statement was created →
account_statement_path(statement)(the statement vault inbox view) - Nothing created →
account_statements_path
The accepted types and the 25 MB cap are constants on AccountStatement referenced directly in the upload form .
Turbo Frame Lazy-Loading (Account Detail Page)#
The Statements tab on the account detail page is lazy-loaded via a named Turbo Frame. The frame ID is dom_id(@account, :statements_tab) — e.g., statements_tab_account_42 .
How it works:
- When the Statements tab is activated, the browser issues a request with
tab=statementsand the matchingTurbo-Frameheader. AccountsController#showdetectsstatement_tab_active?(@tab == "statements") and callsbuild_statement_tab_data:- Builds
AccountStatement::Coverage.for_year(@account, params[:statement_year]) - Loads
@account.account_statements.with_attached_original_file.ordered.to_a - Pre-computes
AccountStatement.reconciliation_statuses_for(...)to avoid N+1
- Builds
- If the request is a Turbo Frame request targeting the statements frame,
render_statement_tab_framereturns only the_statements_framepartial , skipping the full account-page render. _statements_frame.html.erbwraps_statements.html.erbin theturbo_frame_tag.
Year filter: An auto-submit form inside _statements.html.erb submits statement_year + tab=statements on change, re-triggering the same frame request path for a different year .
Permission guard: @can_manage_statements requires both AccountStatement.statement_manager?(Current.user) and permission.in?([:owner, :full_control]) for the specific account . Read-only users see the coverage grid and statement list but not the upload form or unlink button .
Testing Infrastructure#
uploaded_file Helper#
The global test helper uploaded_file (defined in test/test_helper.rb, available to all ActiveSupport::TestCase subclasses) creates a Rack::Test::UploadedFile from a temp file:
def uploaded_file(filename:, content_type:, content: "date,amount\n2024-01-01,1\n")
It defaults to minimal valid CSV content, making it easy to create realistic upload params without managing temp files manually. Tests override content: when they need specific data (e.g., duplicate detection, oversized file validation).
Controller Test Patterns#
AccountStatementsControllerTest is an ActionDispatch::IntegrationTest covering:
- Authorization: manager vs. member vs. guest access, cross-family account ID rejection
- Upload loop behavior: continues after a validation error or invalid file type; collects all errors/successes before redirecting
- Deduplication: asserts no new record and a flash alert for duplicate content
- Redirect semantics: linked uploads go to
account_path(account, tab: "statements"); unmatched uploads go to the statementshowpage - Link / unlink / reject: verifies status transitions and redirect destinations
- Metadata update: covers field persistence and account linking via
update - Read-only view: asserts absence of edit controls for
family_memberon a read-only account
Each test uses AccountStatement.create_from_upload! to set up fixtures inline rather than relying on YAML fixtures, keeping test state explicit.
Key Files#
| File | Role |
|---|---|
app/controllers/account_statements_controller.rb | All 8 CRUD + link/unlink/reject actions |
app/controllers/accounts_controller.rb | build_statement_tab_data, render_statement_tab_frame, lazy-load guard |
app/views/accounts/show/_statements_frame.html.erb | Turbo Frame container (dom_id(@account, :statements_tab)) |
app/views/accounts/show/_statements.html.erb | Coverage grid, upload form, statement table, year filter |
app/models/account_statement.rb | prepare_upload!, create_from_prepared_upload!, validations, status transitions |
app/models/account_statement/coverage.rb | Monthly coverage grid data |
test/controllers/account_statements_controller_test.rb | Integration tests for all actions |
test/test_helper.rb | uploaded_file helper |