Context
PR #1698 (Complete Sophtron account mapping) was merged with the following known issues deferred for follow-up. This issue tracks them as a single body of work for the next Sophtron PR.
Bugs
1. Wrong pluralization word in transactions_failed error message
File: app/models/sophtron_item/syncer.rb ~line 123
'account'.pluralize is used for a transactions_failed count, producing messages like "2 accounts failed to import transactions" instead of "2 transactions failed to import transactions".
# Current (wrong)
message: "#{import_result[:transactions_failed]} #{'account'.pluralize(import_result[:transactions_failed])} failed to import transactions"
# Fix
message: "#{import_result[:transactions_failed]} #{'transaction'.pluralize(import_result[:transactions_failed])} failed to import"
2. Unguarded update! inside rescue Provider::Sophtron::Error blocks
File: app/models/sophtron_item/importer.rb — fetch_and_store_transactions and refresh_account_before_transaction_fetch
Both rescue blocks call sophtron_item.update!(status: :requires_update) without an inner rescue. If update! raises ActiveRecord::RecordInvalid, it escapes the outer rescue and turns a recoverable auth failure into a hard job crash.
# Current (dangerous)
rescue Provider::Sophtron::Error => e
requires_update = e.error_type.in?([ :unauthorized, :access_forbidden ])
sophtron_item.update!(status: :requires_update) if requires_update
{ success: false, ... }
# Fix: use non-bang update, or add inner rescue
sophtron_item.update(status: :requires_update) if requires_update
3. Behavioral edge case in initial_transaction_fetch? for newly-added accounts
File: app/models/sophtron_item/importer.rb
The current condition is:
sophtron_account.raw_transactions_payload.nil? && sophtron_item.last_synced_at.blank?
A newly-linked account added to an already-synced item has raw_transactions_payload.nil? but last_synced_at present, so it falls into the incremental path and gets only last_synced_at - buffer of history instead of a full historical load. The original code comment said "treat it as a first sync for this account even if the item has been synced before."
Action needed: Confirm whether this is intentional. If not, change to sophtron_account.raw_transactions_payload.nil? alone (account-scoped check only).
Security / Privacy
4. Raw Sophtron API response bodies exposed in browser flash alerts
Files: app/models/provider/sophtron.rb + app/controllers/sophtron_items_controller.rb
handle_response embeds the raw API response body in exception messages:
raise Error.new("Bad request to Sophtron API: #{body}", :bad_request, details: body)
raise Error.new("Sophtron API request failed: #{response.code} #{response.message} - #{body}", :fetch_failed, details: body)
Controller rescue blocks then forward e.message directly to user-visible flash alerts:
redirect_to ..., alert: t(".api_error", message: e.message)
This means raw Sophtron API bodies (which can include MFA challenge content, auth error payloads, and backend error details) reach the user's browser. The body is already available in details: for diagnostics — remove it from the human-readable message string.
# Fix
raise Error.new("Bad request to Sophtron API", :bad_request, details: body)
raise Error.new("Sophtron API request failed: #{response.code} #{response.message}", :fetch_failed, details: body)
5. normalize_base_url silently falls back to DEFAULT_BASE_URL on invalid URI
File: app/models/provider/sophtron.rb
A misconfigured base URL (invalid scheme, garbage value) is silently swallowed and replaced with the production Sophtron URL, causing live credentials to be sent to the wrong endpoint rather than surfacing a configuration error.
# Current
rescue URI::InvalidURIError
DEFAULT_BASE_URL
# Fix
rescue URI::InvalidURIError => e
raise Error.new("Invalid Sophtron base URL: #{e.message}", :configuration_error)
6. Rails.logger.debug logs full account data on every page render
File: app/views/sophtron_items/select_accounts.html.erb line ~18
<% Rails.logger.debug "Sophtron account data: #{account.inspect}" %>
account.inspect includes account numbers, balances, and institution names. This line predates PR #1698 but was not removed. Delete it or restrict to Rails.env.development?.
i18n
7. Hardcoded English strings in sophtron_item/syncer.rb import_errors_for
File: app/models/sophtron_item/syncer.rb ~lines 115, 123, 128
Three raw English strings violate the mandatory i18n convention:
message: "#{n} #{'account'.pluralize(n)} failed to import"
message: "#{n} #{'account'.pluralize(n)} failed to import transactions"
errors.presence || [ { message: "Sophtron import failed", category: "sync_error" } ]
Move to config/locales/en.yml under sophtron_items.syncer.* and use t() with Rails pluralization.
8. Hardcoded English strings in last_connection_error from background jobs
Files: app/jobs/sophtron_refresh_poll_job.rb, app/models/sophtron_item/importer.rb
Several places write hardcoded English directly to last_connection_error, which is rendered to users in the sync panel:
sophtron_item.update!(last_connection_error: "Sophtron refresh failed")
sophtron_item.update!(last_connection_error: "Sophtron refresh did not finish before the polling timeout")
last_connection_error: "Sophtron refresh requires MFA"
These should use locale keys, consistent with items 7 above.
Code Quality
9. first_present helper duplicated in two files
Files: app/models/provider/sophtron.rb, app/models/sophtron_account.rb
Identical private first_present(hash, *keys) method defined independently in both classes. Extract to a shared module (e.g. Provider::Sophtron::PayloadHelpers or a concern).
10. Fat controller — domain logic in private controller methods
File: app/controllers/sophtron_items_controller.rb
The controller has ~30 private methods including domain logic that belongs in the model layer per project conventions ("Skinny Controllers, Fat Models"):
build_mfa_challenge → SophtronItem#mfa_challenge
post_mfa_job_payload? / login_progress_job_payload? → SophtronItem predicates
sophtron_connection_failure_message → SophtronItem#connection_failure_message
fetch_remote_accounts → SophtronItem#remote_accounts(force:)
persist_remote_sophtron_accounts → SophtronItem#persist_remote_accounts
This can be tackled incrementally alongside other model-layer work.
Priority
| # |
Item |
Priority |
| 1 |
Wrong pluralization word |
High — produces incorrect UI copy |
| 2 |
Unguarded update! in rescue blocks |
High — turns auth failures into job crashes |
| 4 |
Raw API body in flash alerts |
High — privacy/security |
| 7 |
Hardcoded strings in import_errors_for |
Medium — i18n violation |
| 8 |
Hardcoded strings in last_connection_error |
Medium — i18n violation |
| 5 |
normalize_base_url silent fallback |
Medium — misconfiguration hazard |
| 3 |
initial_transaction_fetch? edge case |
Medium — confirm intent first |
| 6 |
Rails.logger.debug in view |
Low — pre-existing, debug level only |
| 9 |
first_present duplication |
Low — code smell |
| 10 |
Fat controller |
Low — can be done incrementally |
Items 1, 2, and 4 are the most impactful and can be fixed in a small focused PR.
Tracked from code review of PR #1698.
Context
PR #1698 (Complete Sophtron account mapping) was merged with the following known issues deferred for follow-up. This issue tracks them as a single body of work for the next Sophtron PR.
Bugs
1. Wrong pluralization word in
transactions_failederror messageFile:
app/models/sophtron_item/syncer.rb~line 123'account'.pluralizeis used for atransactions_failedcount, producing messages like "2 accounts failed to import transactions" instead of "2 transactions failed to import transactions".2. Unguarded
update!insiderescue Provider::Sophtron::ErrorblocksFile:
app/models/sophtron_item/importer.rb—fetch_and_store_transactionsandrefresh_account_before_transaction_fetchBoth rescue blocks call
sophtron_item.update!(status: :requires_update)without an inner rescue. Ifupdate!raisesActiveRecord::RecordInvalid, it escapes the outer rescue and turns a recoverable auth failure into a hard job crash.3. Behavioral edge case in
initial_transaction_fetch?for newly-added accountsFile:
app/models/sophtron_item/importer.rbThe current condition is:
A newly-linked account added to an already-synced item has
raw_transactions_payload.nil?butlast_synced_atpresent, so it falls into the incremental path and gets onlylast_synced_at - bufferof history instead of a full historical load. The original code comment said "treat it as a first sync for this account even if the item has been synced before."Action needed: Confirm whether this is intentional. If not, change to
sophtron_account.raw_transactions_payload.nil?alone (account-scoped check only).Security / Privacy
4. Raw Sophtron API response bodies exposed in browser flash alerts
Files:
app/models/provider/sophtron.rb+app/controllers/sophtron_items_controller.rbhandle_responseembeds the raw API response body in exception messages:Controller rescue blocks then forward
e.messagedirectly to user-visible flash alerts:This means raw Sophtron API bodies (which can include MFA challenge content, auth error payloads, and backend error details) reach the user's browser. The body is already available in
details:for diagnostics — remove it from the human-readablemessagestring.5.
normalize_base_urlsilently falls back toDEFAULT_BASE_URLon invalid URIFile:
app/models/provider/sophtron.rbA misconfigured base URL (invalid scheme, garbage value) is silently swallowed and replaced with the production Sophtron URL, causing live credentials to be sent to the wrong endpoint rather than surfacing a configuration error.
6.
Rails.logger.debuglogs full account data on every page renderFile:
app/views/sophtron_items/select_accounts.html.erbline ~18account.inspectincludes account numbers, balances, and institution names. This line predates PR #1698 but was not removed. Delete it or restrict toRails.env.development?.i18n
7. Hardcoded English strings in
sophtron_item/syncer.rbimport_errors_forFile:
app/models/sophtron_item/syncer.rb~lines 115, 123, 128Three raw English strings violate the mandatory i18n convention:
Move to
config/locales/en.ymlundersophtron_items.syncer.*and uset()with Rails pluralization.8. Hardcoded English strings in
last_connection_errorfrom background jobsFiles:
app/jobs/sophtron_refresh_poll_job.rb,app/models/sophtron_item/importer.rbSeveral places write hardcoded English directly to
last_connection_error, which is rendered to users in the sync panel:These should use locale keys, consistent with items 7 above.
Code Quality
9.
first_presenthelper duplicated in two filesFiles:
app/models/provider/sophtron.rb,app/models/sophtron_account.rbIdentical private
first_present(hash, *keys)method defined independently in both classes. Extract to a shared module (e.g.Provider::Sophtron::PayloadHelpersor a concern).10. Fat controller — domain logic in private controller methods
File:
app/controllers/sophtron_items_controller.rbThe controller has ~30 private methods including domain logic that belongs in the model layer per project conventions ("Skinny Controllers, Fat Models"):
build_mfa_challenge→SophtronItem#mfa_challengepost_mfa_job_payload?/login_progress_job_payload?→SophtronItempredicatessophtron_connection_failure_message→SophtronItem#connection_failure_messagefetch_remote_accounts→SophtronItem#remote_accounts(force:)persist_remote_sophtron_accounts→SophtronItem#persist_remote_accountsThis can be tackled incrementally alongside other model-layer work.
Priority
update!in rescue blocksimport_errors_forlast_connection_errornormalize_base_urlsilent fallbackinitial_transaction_fetch?edge caseRails.logger.debugin viewfirst_presentduplicationItems 1, 2, and 4 are the most impactful and can be fixed in a small focused PR.
Tracked from code review of PR #1698.