Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
df7a137
Add HTTParty gem for SimpleFin API integration
Himmelschmidt Aug 7, 2025
9561e73
Add SimpleFin database schema
Himmelschmidt Aug 7, 2025
8497703
Implement SimpleFin API client and data models
Himmelschmidt Aug 7, 2025
30d0b25
Update core models for SimpleFin integration
Himmelschmidt Aug 7, 2025
18b69a4
Add SimpleFin controllers and routing
Himmelschmidt Aug 7, 2025
35693e5
Add SimpleFin user interface components
Himmelschmidt Aug 7, 2025
71d720b
Add user account type selection workflow
Himmelschmidt Aug 7, 2025
2caba0a
Add tests for SimpleFin integration
Himmelschmidt Aug 7, 2025
8192481
Fix account show page for SimpleFin accounts
Himmelschmidt Aug 7, 2025
dbcbf89
Complete subtype selection for SimpleFin accounts
Himmelschmidt Aug 7, 2025
332f5b0
Fix dark mode compatibility for SimpleFin UI components
Himmelschmidt Aug 7, 2025
9e557df
Fix SimpleFin integration bugs and improve code quality
Himmelschmidt Aug 7, 2025
aea9b06
Remove SimpleFin demo file with outdated setup token
Himmelschmidt Aug 7, 2025
0c14cca
Update SimpleFin User-Agent to use Sure Finance branding
Himmelschmidt Aug 7, 2025
00cb130
Remove unused SimpleFin account type mapping logic
Himmelschmidt Aug 7, 2025
43b3399
Gate SimpleFin option behind US region check
Himmelschmidt Aug 7, 2025
4f6b738
Refactor SimpleFin controller to use model method
Himmelschmidt Aug 7, 2025
1d67188
Fix critical data integrity issue in SimpleFin date parsing
Himmelschmidt Aug 7, 2025
7fee3fe
Address all Gemini code review feedback for SimpleFin integration
Himmelschmidt Aug 7, 2025
e92751d
Fix tests for subtype delegation to accountable models
Himmelschmidt Aug 7, 2025
7685d95
Fix trailing whitespace (rubocop auto-fix)
Himmelschmidt Aug 7, 2025
8776faf
Add option to "skip" adding an account
jjmata Aug 7, 2025
2717200
Revert "Gate SimpleFin option behind US region check"
Himmelschmidt Aug 7, 2025
a088a7c
Fix SimpleFin transaction syncing and clean up debug logging
Himmelschmidt Aug 8, 2025
e8b54bc
Not sure how skipping worked for me the first time
jjmata Aug 8, 2025
6681537
Fix SimpleFin new account setup flow and UI dark mode issues
Himmelschmidt Aug 8, 2025
77adfc4
Fix linter issues (trailing whitespace and ERB formatting)
Himmelschmidt Aug 8, 2025
020a261
Remove SimpleFin button from create accounts view
Himmelschmidt Aug 8, 2025
d93235b
Add missing SimpleFin attributes and fix balance attribute error
Himmelschmidt Aug 8, 2025
d78b172
Store all SimpleFin protocol fields in JSONB following existing patterns
Himmelschmidt Aug 8, 2025
83994b0
Fix SimpleFin API date parameter format and improve error handling
Himmelschmidt Aug 11, 2025
2d82239
Implement comprehensive historical transaction sync for SimpleFin
Himmelschmidt Aug 11, 2025
b8c2edb
Fix SimpleFin transaction sign convention to match Maybe's format
Himmelschmidt Aug 11, 2025
b8b6acb
Add SimpleFin account association and fix balance handling for liabil…
Himmelschmidt Aug 11, 2025
877ea26
Display organization names throughout SimpleFin interface
Himmelschmidt Aug 11, 2025
0dfa0b7
Fix SimpleFin UI styling to match design system
Himmelschmidt Aug 11, 2025
8bd2fd0
Spelling
jjmata Aug 11, 2025
d2cbea7
Merge branch 'main' into feature/simplefin-integration
jjmata Aug 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ gem "csv"
gem "redcarpet"
gem "stripe"
gem "plaid"
gem "httparty"
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider pinning HTTParty to a specific version range.

Adding httparty without version constraints could lead to unexpected behavior if breaking changes are introduced in future releases. For production stability, it's recommended to specify a version constraint.

Apply this diff to pin HTTParty to a stable version range:

-gem "httparty"
+gem "httparty", "~> 0.22.0"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
gem "httparty"
-gem "httparty"
+gem "httparty", "~> 0.22.0"
🤖 Prompt for AI Agents
In Gemfile around line 72, the httparty gem is added without a version
constraint; update that line to pin httparty to a stable version range (for
example "~> 0.18" or another vetted range used by the project), then run bundle
install (or bundle update httparty) to update Gemfile.lock and run the test
suite to ensure compatibility.

gem "rotp", "~> 6.3"
gem "rqrcode", "~> 3.0"
gem "activerecord-import"
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ GEM
turbo-rails (>= 1.2)
htmlbeautifier (1.4.3)
htmlentities (4.3.4)
httparty (0.23.1)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.15)
Expand Down Expand Up @@ -335,6 +339,8 @@ GEM
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
msgpack (1.8.0)
multi_xml (0.7.2)
bigdecimal (~> 3.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
net-http (0.6.0)
Expand Down Expand Up @@ -648,6 +654,7 @@ DEPENDENCIES
foreman
hotwire-livereload
hotwire_combobox
httparty
i18n-tasks
image_processing (>= 1.2)
importmap-rails
Expand Down
1 change: 1 addition & 0 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class AccountsController < ApplicationController
def index
@manual_accounts = family.accounts.manual.alphabetically
@plaid_items = family.plaid_items.ordered
@simplefin_items = family.simplefin_items.ordered

render layout: "settings"
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/properties_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def address_params

def property_params
params.require(:account)
.permit(:name, :subtype, :accountable_type, accountable_attributes: [ :id, :year_built, :area_unit, :area_value ])
.permit(:name, :accountable_type, accountable_attributes: [ :id, :subtype, :year_built, :area_unit, :area_value ])
end

def set_property
Expand Down
139 changes: 139 additions & 0 deletions app/controllers/simplefin_items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
class SimplefinItemsController < ApplicationController
before_action :set_simplefin_item, only: [ :show, :destroy, :sync, :setup_accounts, :complete_account_setup ]

def index
@simplefin_items = Current.family.simplefin_items.active.ordered
render layout: "settings"
end

def show
end

def new
@simplefin_item = Current.family.simplefin_items.build
end

def create
setup_token = simplefin_params[:setup_token]

return render_error("Please enter a SimpleFin setup token.") if setup_token.blank?

begin
@simplefin_item = Current.family.create_simplefin_item!(
setup_token: setup_token,
item_name: "SimpleFin Connection"
)

redirect_to simplefin_items_path, notice: "SimpleFin connection added successfully! Your accounts will appear shortly as they sync in the background."
rescue ArgumentError, URI::InvalidURIError
render_error("Invalid setup token. Please check that you copied the complete token from SimpleFin Bridge.", setup_token)
rescue Provider::Simplefin::SimplefinError => e
error_message = case e.error_type
when :token_compromised
"The setup token may be compromised, expired, or already used. Please create a new one."
else
"Failed to connect: #{e.message}"
end
render_error(error_message, setup_token)
rescue => e
Rails.logger.error("SimpleFin connection error: #{e.message}")
render_error("An unexpected error occurred. Please try again or contact support.", setup_token)
end
end

def destroy
@simplefin_item.destroy_later
redirect_to simplefin_items_path, notice: "SimpleFin connection will be removed"
end

def sync
@simplefin_item.sync_later
redirect_to simplefin_item_path(@simplefin_item), notice: "Sync started"
end

def setup_accounts
@simplefin_accounts = @simplefin_item.simplefin_accounts.includes(:account).where(accounts: { id: nil })
@account_type_options = [
[ "Checking or Savings Account", "Depository" ],
[ "Credit Card", "CreditCard" ],
[ "Investment Account", "Investment" ],
[ "Loan or Mortgage", "Loan" ],
[ "Other Asset", "OtherAsset" ],
[ "Skip - don't add", "Skip" ]
]

# Subtype options for each account type
@subtype_options = {
"Depository" => {
label: "Account Subtype:",
options: Depository::SUBTYPES.map { |k, v| [ v[:long], k ] }
},
"CreditCard" => {
label: "",
options: [],
message: "Credit cards will be automatically set up as credit card accounts."
},
"Investment" => {
label: "Investment Type:",
options: Investment::SUBTYPES.map { |k, v| [ v[:long], k ] }
},
"Loan" => {
label: "Loan Type:",
options: Loan::SUBTYPES.map { |k, v| [ v[:long], k ] }
},
"OtherAsset" => {
label: nil,
options: [],
message: "No additional options needed for Other Assets."
}
}
end

def complete_account_setup
account_types = params[:account_types] || {}
account_subtypes = params[:account_subtypes] || {}

account_types.each do |simplefin_account_id, selected_type|
# Skip accounts that the user chose not to add
next if selected_type == "Skip"
Comment on lines +92 to +98
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the constant reference error that causes crashes when accessing credit card accounts

Based on the PR comments, users are encountering uninitialized constant Skip errors. The issue is on line 98 where selected_type == "Skip" is being checked as a string, but the error suggests the code might be trying to reference it as a constant elsewhere.

The error is likely not in this exact line but related to how the Skip value is being processed. Let me verify if there's an issue with the account creation logic that might be causing the reported errors.

#!/bin/bash
# Check for any references to Skip as a constant in the codebase
rg "Skip[^'\"]" --type ruby

# Check the Account model's create_from_simplefin_account method implementation
ast-grep --pattern 'def create_from_simplefin_account($$$)'

# Look for any Skip-related constant definitions
rg "^\s*(class|module)\s+Skip" --type ruby


simplefin_account = @simplefin_item.simplefin_accounts.find(simplefin_account_id)
selected_subtype = account_subtypes[simplefin_account_id]

# Default subtype for CreditCard since it only has one option
selected_subtype = "credit_card" if selected_type == "CreditCard" && selected_subtype.blank?

# Create account with user-selected type and subtype
account = Account.create_from_simplefin_account(
simplefin_account,
selected_type,
selected_subtype
)
simplefin_account.update!(account: account)
end

# Clear pending status and mark as complete
@simplefin_item.update!(pending_account_setup: false)

# Schedule account syncs for the newly created accounts
@simplefin_item.schedule_account_syncs

redirect_to simplefin_items_path, notice: "SimpleFin accounts have been set up successfully!"
end

private

def set_simplefin_item
@simplefin_item = Current.family.simplefin_items.find(params[:id])
end

def simplefin_params
params.require(:simplefin_item).permit(:setup_token)
end

def render_error(message, setup_token = nil)
@simplefin_item = Current.family.simplefin_items.build(setup_token: setup_token)
@error_message = message
render :new, status: :unprocessable_entity
end
end
10 changes: 10 additions & 0 deletions app/helpers/accounts_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@ def summary_card(title:, &block)
content = capture(&block)
render "accounts/summary_card", title: title, content: content
end

def sync_path_for(account)
if account.plaid_account_id.present?
sync_plaid_item_path(account.plaid_account.plaid_item)
elsif account.simplefin_account_id.present?
sync_simplefin_item_path(account.simplefin_account.simplefin_item)
else
sync_account_path(account)
end
end
end
45 changes: 45 additions & 0 deletions app/javascript/controllers/account_type_selector_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["subtypeContainer"]
static values = { accountId: String }

connect() {
// Show initial subtype dropdown based on current selection
this.updateSubtype()
}

updateSubtype(event) {
const selectElement = this.element.querySelector('select[name^="account_types"]')
const selectedType = selectElement ? selectElement.value : ''
const container = this.subtypeContainerTarget
const accountId = this.accountIdValue

// Hide all subtype selects
const subtypeSelects = container.querySelectorAll('.subtype-select')
subtypeSelects.forEach(select => {
select.style.display = 'none'
// Clear the name attribute so it doesn't get submitted
const selectElement = select.querySelector('select')
if (selectElement) {
selectElement.removeAttribute('name')
}
})

// Don't show any subtype select for Skip option
if (selectedType === 'Skip') {
return
}

// Show the relevant subtype select
const relevantSubtype = container.querySelector(`[data-type="${selectedType}"]`)
if (relevantSubtype) {
relevantSubtype.style.display = 'block'
// Re-add the name attribute so it gets submitted
const selectElement = relevantSubtype.querySelector('select')
if (selectElement) {
selectElement.setAttribute('name', `account_subtypes[${accountId}]`)
}
}
}
}
51 changes: 50 additions & 1 deletion app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class Account < ApplicationRecord

belongs_to :family
belongs_to :import, optional: true
belongs_to :simplefin_account, optional: true

has_many :import_mappings, as: :mappable, dependent: :destroy, class_name: "Import::Mapping"
has_many :entries, dependent: :destroy
Expand All @@ -22,11 +23,12 @@ class Account < ApplicationRecord
scope :assets, -> { where(classification: "asset") }
scope :liabilities, -> { where(classification: "liability") }
scope :alphabetically, -> { order(:name) }
scope :manual, -> { where(plaid_account_id: nil) }
scope :manual, -> { where(plaid_account_id: nil, simplefin_account_id: nil) }

has_one_attached :logo

delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
delegate :subtype, to: :accountable, allow_nil: true

accepts_nested_attributes_for :accountable, update_only: true

Expand Down Expand Up @@ -71,6 +73,53 @@ def create_and_sync(attributes)
account.sync_later
account
end


def create_from_simplefin_account(simplefin_account, account_type, subtype = nil)
# Get the balance from SimpleFin
balance = simplefin_account.current_balance || simplefin_account.available_balance || 0

# SimpleFin returns negative balances for credit cards (liabilities)
# But Maybe expects positive balances for liabilities
if account_type == "CreditCard" || account_type == "Loan"
balance = balance.abs
end
Comment on lines +84 to +86
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify balance conversion logic

The comment states SimpleFin returns negative balances for credit cards, but the logic uses abs for both CreditCard and Loan types. Verify this is correct for loans as well.


🌐 Web query:

Does SimpleFin API return negative balances for loan accounts similar to credit cards, or only for credit card accounts?

💡 Result:

Short answer: SimpleFIN itself does not guarantee that loan account balances will be returned as negative. The SimpleFIN protocol documents the balance and transaction-sign conventions but does not mandate “loan = negative” semantics — institutions/servers can and do return either sign. (simplefin.org)

Supporting details and recommendations

  • The SimpleFIN spec defines "balance" as a numeric string and states that transaction amounts use the convention "positive = money deposited into the account" (so signs are relative to cash flow), but it does not spell out that loans must be negative like some other APIs do. That means how a loan or credit-card balance is signed is up to the server implementation. (simplefin.org)
  • In practice developers have reported inconsistent signs for liability accounts (credit cards, car loans, mortgages) when importing via SimpleFIN — some institutions return positive balances for loans or credit cards, which can make them look like assets unless you normalize them. (github.com, reddit.com)
  • By contrast, APIs such as Plaid explicitly document sign semantics for credit and loan accounts (credit: positive = amount owed; negative = lender owes you; loans: typically positive = principal remaining, negative only if lender owes you). If you need that behavior, you should normalize SimpleFIN data in your app. (plaid.com)

Practical approach (recommended)

  • Treat the value from SimpleFIN as “raw.” Detect whether the account is a liability (credit card / loan / mortgage) from whatever metadata your SimpleFIN server provides (account name/type in extra fields or org conventions). Then convert to the sign convention your app expects (for example, if you want liabilities to appear negative for net-worth calculations, set normalized_balance = -abs(raw_balance) for liabilities).
  • Always test with the specific institutions you plan to support — SimpleFIN servers vary by bank and you’ll see different behavior across providers. (simplefin.org, github.com)

If you want, tell me:

  • which institutions or account types you’re integrating and what sign convention your app expects, and I’ll suggest specific normalization logic or sample code.

Citations:


Verify liability account sign normalization

The SimpleFIN spec does not guarantee that loan balances will be returned as negative—implementations vary by institution. Right now you’re calling balance.abs for both credit-card and loan accounts, which forces a positive value whether SimpleFIN returns + or -. That’s fine if your app treats liabilities as positive “debt” amounts, but if you need liabilities to appear negative (e.g. for net-worth calculations), you should instead normalize to -balance.abs.

Please confirm your intended sign convention and adjust accordingly:

  • If you want liabilities (CreditCard and Loan) to be negative:
    if %w[CreditCard Loan].include?(account_type)
      balance = -balance.abs
    end
  • If you want liabilities to be shown as positive debt amounts, leaving the code as-is (balance.abs) is correct.
🤖 Prompt for AI Agents
In app/models/account.rb around lines 84 to 86, the code currently forces
liabilities (CreditCard and Loan) to be positive by calling balance.abs, but the
app’s sign convention for liabilities must be explicit; if you want liabilities
represented as negative amounts for net-worth calculations, change the
normalization to set balance = -balance.abs for those account types, otherwise
leave as balance.abs if you intend liabilities to be shown as positive debt
amounts—update the conditional accordingly (use a clear %w[...] include? check)
and ensure tests reflect the chosen convention.


attributes = {
family: simplefin_account.simplefin_item.family,
name: simplefin_account.name,
balance: balance,
currency: simplefin_account.currency,
accountable_type: account_type,
accountable_attributes: build_simplefin_accountable_attributes(simplefin_account, account_type, subtype),
simplefin_account_id: simplefin_account.id
}

create_and_sync(attributes)
end


private

def build_simplefin_accountable_attributes(simplefin_account, account_type, subtype)
attributes = {}
attributes[:subtype] = subtype if subtype.present?

# Set account-type-specific attributes from SimpleFin data
case account_type
when "CreditCard"
# For credit cards, available_balance often represents available credit
if simplefin_account.available_balance.present? && simplefin_account.available_balance > 0
attributes[:available_credit] = simplefin_account.available_balance
Comment on lines +112 to +113
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Potential bug with balance logic for credit cards

Based on the PR comments, there's an issue with credit card accounts showing "unknown attribute 'balance' for CreditCard". The condition checks if available_balance > 0 before setting available_credit, but for credit cards, the available balance from SimpleFin might be negative (representing available credit). This logic might need adjustment.

         when "CreditCard"
           # For credit cards, available_balance often represents available credit
-          if simplefin_account.available_balance.present? && simplefin_account.available_balance > 0
-            attributes[:available_credit] = simplefin_account.available_balance
+          # SimpleFin may return negative values for available credit
+          if simplefin_account.available_balance.present?
+            attributes[:available_credit] = simplefin_account.available_balance.abs
           end

🏁 Script executed:

#!/bin/bash
# Check if CreditCard model has a balance attribute or if it's only on Account
echo "Checking CreditCard model attributes..."
rg "class CreditCard" app/models --type ruby -A 20

# Check schema for credit_cards table
rg "create_table.*credit_cards" db/schema.rb -A 15

# Check if there's a balance attribute definition
rg "attribute.*balance|attr_.*balance" app/models/credit_card.rb

Length of output: 1838


Update credit card available_credit logic to handle negative balances

For CreditCard accounts, SimpleFin may return a negative available_balance to represent available credit. Remove the > 0 guard and assign the absolute value instead.

• File: app/models/account.rb (around lines 112–115)

         when "CreditCard"
           # SimpleFin may return negative values for available credit
-          if simplefin_account.available_balance.present? && simplefin_account.available_balance > 0
-            attributes[:available_credit] = simplefin_account.available_balance
+          if simplefin_account.available_balance.present?
+            attributes[:available_credit] = simplefin_account.available_balance.abs
           end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if simplefin_account.available_balance.present? && simplefin_account.available_balance > 0
attributes[:available_credit] = simplefin_account.available_balance
when "CreditCard"
# SimpleFin may return negative values for available credit
if simplefin_account.available_balance.present?
attributes[:available_credit] = simplefin_account.available_balance.abs
end
🤖 Prompt for AI Agents
In app/models/account.rb around lines 112–113, the current guard only assigns
available_credit when simplefin_account.available_balance > 0; update it so
CreditCard accounts handle negative balances by removing the `> 0` guard and
assigning the absolute value instead: when
simplefin_account.available_balance.present? and the account is a CreditCard,
set attributes[:available_credit] = simplefin_account.available_balance.abs; for
non-credit-card accounts preserve the existing behavior (only assign when
balance > 0).

end
when "Loan"
# For loans, we might get additional data from the raw_payload
# This is where loan-specific information could be extracted if available
# Currently we don't have specific loan fields from SimpleFin protocol
end

attributes
end
end

def institution_domain
Expand Down
5 changes: 3 additions & 2 deletions app/models/account/linkable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ module Account::Linkable

included do
belongs_to :plaid_account, optional: true
belongs_to :simplefin_account, optional: true
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Association looks good; add exclusivity guard to prevent double-linking (Plaid + SimpleFin).

Right now an account could potentially have both plaid_account_id and simplefin_account_id set. Enforce mutual exclusivity to avoid ambiguous data sources.

Apply this diff:

   included do
     belongs_to :plaid_account, optional: true
     belongs_to :simplefin_account, optional: true
+    validate :provider_link_exclusivity
   end
+
+  private
+    def provider_link_exclusivity
+      if plaid_account_id.present? && simplefin_account_id.present?
+        errors.add(:base, "Account cannot be linked to multiple providers (Plaid and SimpleFin)")
+      end
+    end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
belongs_to :simplefin_account, optional: true
# app/models/account/linkable.rb
module Account
module Linkable
extend ActiveSupport::Concern
included do
belongs_to :plaid_account, optional: true
belongs_to :simplefin_account, optional: true
validate :provider_link_exclusivity
end
private
def provider_link_exclusivity
if plaid_account_id.present? && simplefin_account_id.present?
errors.add(
:base,
"Account cannot be linked to multiple providers (Plaid and SimpleFin)"
)
end
end
end
end
🤖 Prompt for AI Agents
In app/models/account/linkable.rb around line 6, currently the model allows both
plaid_account_id and simplefin_account_id to be set; add a validation to enforce
mutual exclusivity so an account cannot be linked to both sources
simultaneously. Implement a custom validator method (e.g., validate
:ensure_single_account_source) that adds an error on :base when both
plaid_account_id and simplefin_account_id are present, and ensure the model
returns invalid in that case; update or add specs to cover the validation.

end

# A "linked" account gets transaction and balance data from a third party like Plaid
# A "linked" account gets transaction and balance data from a third party like Plaid or SimpleFin
def linked?
plaid_account_id.present?
plaid_account_id.present? || simplefin_account_id.present?
end

# An "offline" or "unlinked" account is one where the user tracks values and
Expand Down
2 changes: 1 addition & 1 deletion app/models/family.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Family < ApplicationRecord
include PlaidConnectable, Syncable, AutoTransferMatchable, Subscribeable
include PlaidConnectable, SimplefinConnectable, Syncable, AutoTransferMatchable, Subscribeable

DATE_FORMATS = [
[ "MM-DD-YYYY", "%m-%d-%Y" ],
Expand Down
25 changes: 25 additions & 0 deletions app/models/family/simplefin_connectable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Family::SimplefinConnectable
extend ActiveSupport::Concern

included do
has_many :simplefin_items, dependent: :destroy
end

def can_connect_simplefin?
true # SimpleFin doesn't have regional restrictions like Plaid
end

def create_simplefin_item!(setup_token:, item_name: nil)
simplefin_provider = Provider::Simplefin.new
access_url = simplefin_provider.claim_access_url(setup_token)

simplefin_item = simplefin_items.create!(
name: item_name || "SimpleFin Connection",
access_url: access_url
)

simplefin_item.sync_later

simplefin_item
end
end
Loading