Transaction Management#
Transaction management in Sure covers two intersecting concerns: a unified form that handles income, expense, and transfer entry in a single UI surface, and an attribute-locking system that protects manually created or edited transactions from being overwritten by automated account syncs.
Unified Form Architecture#
The transaction creation form (_form.html.erb) is shared across all three transaction types — Expense, Income, and Transfer — via a three-tab segmented control rendered from _transaction_type_tabs.html.erb.
How type switching works:
- Expense and Income tabs are handled client-side by the Stimulus
TransactionTypeTabsController. Clicking a tab callsselectTab, which updates the hiddennaturefield to either"outflow"(expense) or"inflow"(income) without a page navigation. The active segment styling is also toggled client-side . - The Transfer tab is a plain link to
new_transfer_path— transfers use a separate form and are routed toTransfer::Creatorrather thanTransactionsController.
How nature drives amount sign:
On submission, TransactionsController#entry_params reads the nature value and converts the unsigned form amount into a signed amount before saving :
"inflow"→ negative amount (income/credit)"outflow"→ positive amount (expense/debit)
The form also includes a collapsible Details section for merchant, tags, and notes , and an exchange-rate widget for multi-currency transactions .
Permitted entry params (create/update): name, date, amount, currency, excluded, notes, nature, entryable_type, with nested entryable_attributes for category_id, merchant_id, kind, investment_activity_label, exchange_rate, and tag_ids .
Transfers are not created through TransactionsController. They are built by Transfer::Creator, which creates a matched pair of Transaction records — one outflow on the source account and one inflow on the destination — inside a single database transaction .
Attribute Locking: Protecting Manual Transactions from Sync Overwrites#
Every time a user creates or edits a transaction, the controller calls three methods on the Entry :
| Call | Purpose |
|---|---|
lock_saved_attributes! | Locks every attribute that was just written, storing a timestamp in the locked_attributes JSONB column |
mark_user_modified! | Sets user_modified: true on the entry row, the primary sync-protection flag |
lock_attr!(:tag_ids) | Separately locks tag associations if any tags are present |
The same three calls are made on update and update_tags .
protected_from_sync? returns true if any of three flags are set :
excluded?— entry is soft-deleted from reportsuser_modified?— user made a manual editimport_locked?— entry was created via file import
Enrichable concern (on both Entry and Transaction / entryable) stores lock state in a locked_attributes JSONB column keyed by attribute name → ISO8601 timestamp . The lock_saved_attributes! on Entry delegates to both the entry row and its entryable .
Unlocking: Users can explicitly re-expose a transaction to future syncs via the unlock action , which calls unlock_for_sync! — clearing user_modified, import_locked, and all locked_attributes on both the entry and its entryable in a single transaction.
Key Files#
| File | Role |
|---|---|
TransactionsController | CRUD actions, param signing, lock/protect calls |
transactions/_form.html.erb | Unified transaction form (expense/income/transfer tabs) |
shared/_transaction_type_tabs.html.erb | Three-tab segmented control |
TransactionTypeTabsController (JS) | Client-side tab switching; updates hidden nature field |
Entry model | lock_saved_attributes!, mark_user_modified!, protected_from_sync?, unlock_for_sync! |
Enrichable concern | Core lock/unlock primitives on locked_attributes JSONB |
Transfer::Creator | Paired dual-transaction creation for transfers |