mirror of
https://github.com/we-promise/sure.git
synced 2026-06-02 09:19:02 +00:00
Lunch flow improvements (#268)
- Add support to link existing account with lunch-flow The account will be promoted to a lunch flow connection now ( TBD if we want to allow un-linking? ) - Add support for proper de-dup at provider import level. This will handle de-dups for Lunch Flow, Plaid and SimpleFIN - Fix plaid account removal on invalid credentials
This commit is contained in:
@@ -566,4 +566,223 @@ class Account::ProviderImportAdapterTest < ActiveSupport::TestCase
|
||||
|
||||
assert_match(/Entry with external_id.*already exists with different entryable type/i, exception.message)
|
||||
end
|
||||
|
||||
test "claims manual transaction when provider syncs matching transaction" do
|
||||
# Create a manual transaction (no external_id or source)
|
||||
manual_entry = @account.entries.create!(
|
||||
date: Date.today,
|
||||
amount: 42.50,
|
||||
currency: "USD",
|
||||
name: "Coffee Shop",
|
||||
entryable: Transaction.new
|
||||
)
|
||||
|
||||
assert_nil manual_entry.external_id
|
||||
assert_nil manual_entry.source
|
||||
|
||||
# Provider syncs a matching transaction - should claim the manual entry, not create new
|
||||
assert_no_difference "@account.entries.count" do
|
||||
entry = @adapter.import_transaction(
|
||||
external_id: "lunchflow_12345",
|
||||
amount: 42.50,
|
||||
currency: "USD",
|
||||
date: Date.today,
|
||||
name: "Coffee Shop - Lunchflow",
|
||||
source: "lunchflow"
|
||||
)
|
||||
|
||||
# Should be the same entry, now claimed by the provider
|
||||
assert_equal manual_entry.id, entry.id
|
||||
assert_equal "lunchflow_12345", entry.external_id
|
||||
assert_equal "lunchflow", entry.source
|
||||
assert_equal "Coffee Shop - Lunchflow", entry.name
|
||||
end
|
||||
end
|
||||
|
||||
test "claims CSV imported transaction when provider syncs matching transaction" do
|
||||
# Create a CSV imported transaction (has import_id but no external_id)
|
||||
import = Import.create!(
|
||||
family: @family,
|
||||
type: "TransactionImport",
|
||||
status: :complete
|
||||
)
|
||||
|
||||
csv_entry = @account.entries.create!(
|
||||
date: Date.today - 1.day,
|
||||
amount: 125.00,
|
||||
currency: "USD",
|
||||
name: "Grocery Store",
|
||||
import: import,
|
||||
entryable: Transaction.new
|
||||
)
|
||||
|
||||
assert_nil csv_entry.external_id
|
||||
assert_nil csv_entry.source
|
||||
assert_equal import.id, csv_entry.import_id
|
||||
|
||||
# Provider syncs a matching transaction - should claim the CSV entry
|
||||
assert_no_difference "@account.entries.count" do
|
||||
entry = @adapter.import_transaction(
|
||||
external_id: "plaid_csv_match",
|
||||
amount: 125.00,
|
||||
currency: "USD",
|
||||
date: Date.today - 1.day,
|
||||
name: "Grocery Store - Plaid",
|
||||
source: "plaid"
|
||||
)
|
||||
|
||||
# Should be the same entry, now claimed by the provider
|
||||
assert_equal csv_entry.id, entry.id
|
||||
assert_equal "plaid_csv_match", entry.external_id
|
||||
assert_equal "plaid", entry.source
|
||||
assert_equal import.id, entry.import_id # Should preserve the import_id
|
||||
end
|
||||
end
|
||||
|
||||
test "does not claim transaction when date does not match" do
|
||||
# Create a manual transaction
|
||||
manual_entry = @account.entries.create!(
|
||||
date: Date.today - 5.days,
|
||||
amount: 50.00,
|
||||
currency: "USD",
|
||||
name: "Restaurant",
|
||||
entryable: Transaction.new
|
||||
)
|
||||
|
||||
# Provider syncs similar transaction but different date - should create new entry
|
||||
assert_difference "@account.entries.count", 1 do
|
||||
entry = @adapter.import_transaction(
|
||||
external_id: "lunchflow_different_date",
|
||||
amount: 50.00,
|
||||
currency: "USD",
|
||||
date: Date.today,
|
||||
name: "Restaurant",
|
||||
source: "lunchflow"
|
||||
)
|
||||
|
||||
# Should be a different entry
|
||||
assert_not_equal manual_entry.id, entry.id
|
||||
end
|
||||
end
|
||||
|
||||
test "does not claim transaction when amount does not match" do
|
||||
# Create a manual transaction
|
||||
manual_entry = @account.entries.create!(
|
||||
date: Date.today,
|
||||
amount: 50.00,
|
||||
currency: "USD",
|
||||
name: "Restaurant",
|
||||
entryable: Transaction.new
|
||||
)
|
||||
|
||||
# Provider syncs similar transaction but different amount - should create new entry
|
||||
assert_difference "@account.entries.count", 1 do
|
||||
entry = @adapter.import_transaction(
|
||||
external_id: "lunchflow_different_amount",
|
||||
amount: 51.00,
|
||||
currency: "USD",
|
||||
date: Date.today,
|
||||
name: "Restaurant",
|
||||
source: "lunchflow"
|
||||
)
|
||||
|
||||
# Should be a different entry
|
||||
assert_not_equal manual_entry.id, entry.id
|
||||
end
|
||||
end
|
||||
|
||||
test "does not claim transaction when currency does not match" do
|
||||
# Create a manual transaction
|
||||
manual_entry = @account.entries.create!(
|
||||
date: Date.today,
|
||||
amount: 50.00,
|
||||
currency: "EUR",
|
||||
name: "Restaurant",
|
||||
entryable: Transaction.new
|
||||
)
|
||||
|
||||
# Provider syncs similar transaction but different currency - should create new entry
|
||||
assert_difference "@account.entries.count", 1 do
|
||||
entry = @adapter.import_transaction(
|
||||
external_id: "lunchflow_different_currency",
|
||||
amount: 50.00,
|
||||
currency: "USD",
|
||||
date: Date.today,
|
||||
name: "Restaurant",
|
||||
source: "lunchflow"
|
||||
)
|
||||
|
||||
# Should be a different entry
|
||||
assert_not_equal manual_entry.id, entry.id
|
||||
end
|
||||
end
|
||||
|
||||
test "does not claim transaction that already has external_id from different provider" do
|
||||
# Create a transaction already synced from SimpleFin
|
||||
simplefin_entry = @adapter.import_transaction(
|
||||
external_id: "simplefin_123",
|
||||
amount: 30.00,
|
||||
currency: "USD",
|
||||
date: Date.today,
|
||||
name: "Gas Station",
|
||||
source: "simplefin"
|
||||
)
|
||||
|
||||
# Provider (Lunchflow) syncs matching transaction - should create new entry, not claim SimpleFin's
|
||||
assert_difference "@account.entries.count", 1 do
|
||||
entry = @adapter.import_transaction(
|
||||
external_id: "lunchflow_gas",
|
||||
amount: 30.00,
|
||||
currency: "USD",
|
||||
date: Date.today,
|
||||
name: "Gas Station",
|
||||
source: "lunchflow"
|
||||
)
|
||||
|
||||
# Should be a different entry because SimpleFin already claimed it
|
||||
assert_not_equal simplefin_entry.id, entry.id
|
||||
assert_equal "lunchflow", entry.source
|
||||
assert_equal "simplefin", simplefin_entry.reload.source
|
||||
end
|
||||
end
|
||||
|
||||
test "claims oldest matching manual transaction when multiple exist" do
|
||||
# Create multiple manual transactions with same date, amount, currency
|
||||
older_entry = @account.entries.create!(
|
||||
date: Date.today,
|
||||
amount: 20.00,
|
||||
currency: "USD",
|
||||
name: "Parking - Old",
|
||||
entryable: Transaction.new,
|
||||
created_at: 2.hours.ago
|
||||
)
|
||||
|
||||
newer_entry = @account.entries.create!(
|
||||
date: Date.today,
|
||||
amount: 20.00,
|
||||
currency: "USD",
|
||||
name: "Parking - New",
|
||||
entryable: Transaction.new,
|
||||
created_at: 1.hour.ago
|
||||
)
|
||||
|
||||
# Provider syncs matching transaction - should claim the oldest one
|
||||
assert_no_difference "@account.entries.count" do
|
||||
entry = @adapter.import_transaction(
|
||||
external_id: "lunchflow_parking",
|
||||
amount: 20.00,
|
||||
currency: "USD",
|
||||
date: Date.today,
|
||||
name: "Parking - Provider",
|
||||
source: "lunchflow"
|
||||
)
|
||||
|
||||
# Should claim the older entry
|
||||
assert_equal older_entry.id, entry.id
|
||||
assert_equal "lunchflow_parking", entry.external_id
|
||||
|
||||
# Newer entry should remain unclaimed
|
||||
assert_nil newer_entry.reload.external_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,4 +16,32 @@ class PlaidItemTest < ActiveSupport::TestCase
|
||||
@plaid_item.destroy
|
||||
end
|
||||
end
|
||||
|
||||
test "destroys item even when Plaid credentials are invalid" do
|
||||
error_response = {
|
||||
"error_code" => "INVALID_API_KEYS",
|
||||
"error_message" => "invalid client_id or secret provided"
|
||||
}.to_json
|
||||
|
||||
plaid_error = Plaid::ApiError.new(code: 400, response_body: error_response)
|
||||
@plaid_provider.expects(:remove_item).raises(plaid_error)
|
||||
|
||||
assert_difference "PlaidItem.count", -1 do
|
||||
@plaid_item.destroy
|
||||
end
|
||||
end
|
||||
|
||||
test "destroys item even when Plaid item not found" do
|
||||
error_response = {
|
||||
"error_code" => "ITEM_NOT_FOUND",
|
||||
"error_message" => "item not found"
|
||||
}.to_json
|
||||
|
||||
plaid_error = Plaid::ApiError.new(code: 400, response_body: error_response)
|
||||
@plaid_provider.expects(:remove_item).raises(plaid_error)
|
||||
|
||||
assert_difference "PlaidItem.count", -1 do
|
||||
@plaid_item.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user