mirror of
https://github.com/we-promise/sure.git
synced 2026-04-08 06:44:52 +00:00
* Implement API v1 Imports controller - Add Api::V1::ImportsController with index, show, and create actions - Add Jbuilder views for index and show - Add integration tests - Implement row generation logic in create action - Update routes * Validate import account belongs to family - Add validation to Import model to ensure account belongs to the same family - Add regression test case in Api::V1::ImportsControllerTest * updating docs to be more detailed * Rescue StandardError instead of bare rescue in ImportsController * Optimize Imports API and fix documentation - Implement rows_count counter cache for Imports - Preload rows in Api::V1::ImportsController#show - Update documentation to show correct OAuth scopes * Fix formatting in ImportsControllerTest * Permit all import parameters and fix unknown attribute error * Restore API routes for auth, chats, and messages * removing pr summary * Fix trailing whitespace and configured? test failure - Update Import#configured? to use rows_count for performance and consistency - Mock rows_count in TransactionImportTest - Fix trailing whitespace in migration * Harden security and fix mass assignment in ImportsController - Handle type and account_id explicitly in create action - Rename import_params to import_config_params for clarity - Validate type against Import::TYPES * Fix MintImport rows_count update and migration whitespace - Update MintImport#generate_rows_from_csv to update rows_count counter cache - Fix trailing whitespace and final newline in AddRowsCountToImports migration * Implement full-screen Drag and Drop CSV import on Transactions page - Add DragAndDropImport Stimulus controller listening on document - Add full-screen overlay with icon and text to Transactions index - Update ImportsController to handle direct file uploads via create action - Add system test for drag and drop functionality * Implement Drag and Drop CSV upload on Import Upload page - Add drag-and-drop-import controller to import/uploads/show - Add full-screen overlay to import/uploads/show - Annotate upload form and input with drag-and-drop targets - Add PR_SUMMARY.md * removing pr summary * Add file validation to ImportsController - Validate file size (max 10MB) and MIME type in create action - Prevent memory exhaustion and invalid file processing - Defined MAX_CSV_SIZE and ALLOWED_MIME_TYPES in Import model * Refactor dragLeave logic with counter pattern to prevent flickering * Extract shared drag-and-drop overlay partial - Create app/views/imports/_drag_drop_overlay.html.erb - Update transactions/index and import/uploads/show to use the partial - Reduce code duplication in views * Update Brakeman and harden ImportsController security - Update brakeman to 7.1.2 - Explicitly handle type assignment in ImportsController#create to avoid mass assignment - Remove :type from permitted import parameters * Fix trailing whitespace in DragAndDropImportTest * Don't commit LLM comments as file * FIX add api validation --------- Co-authored-by: Carlos Adames <cj@Carloss-MacBook-Air.local> Co-authored-by: Juan José Mata <jjmata@jjmata.com> Co-authored-by: sokie <sokysrm@gmail.com>
362 lines
11 KiB
Ruby
362 lines
11 KiB
Ruby
require "test_helper"
|
|
|
|
class TransactionImportTest < ActiveSupport::TestCase
|
|
include ActiveJob::TestHelper, ImportInterfaceTest
|
|
|
|
setup do
|
|
@subject = @import = imports(:transaction)
|
|
end
|
|
|
|
test "uploaded? if raw_file_str is present" do
|
|
@import.expects(:raw_file_str).returns("test").once
|
|
assert @import.uploaded?
|
|
end
|
|
|
|
test "configured? if uploaded and rows are generated" do
|
|
@import.expects(:uploaded?).returns(true).once
|
|
@import.expects(:rows_count).returns(1).once
|
|
assert @import.configured?
|
|
end
|
|
|
|
test "cleaned? if rows are generated and valid" do
|
|
@import.expects(:configured?).returns(true).once
|
|
assert @import.cleaned?
|
|
end
|
|
|
|
test "publishable? if cleaned and mappings are valid" do
|
|
@import.expects(:cleaned?).returns(true).once
|
|
assert @import.publishable?
|
|
end
|
|
|
|
test "imports transactions, categories, tags, and accounts" do
|
|
import = <<~CSV
|
|
date,name,amount,category,tags,account,notes
|
|
01/01/2024,Txn1,100,TestCategory1,TestTag1,TestAccount1,notes1
|
|
01/02/2024,Txn2,200,TestCategory2,TestTag1|TestTag2,TestAccount2,notes2
|
|
01/03/2024,Txn3,300,,,,notes3
|
|
CSV
|
|
|
|
@import.update!(
|
|
raw_file_str: import,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
date_format: "%m/%d/%Y"
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
|
|
@import.mappings.create! key: "TestCategory1", create_when_empty: true, type: "Import::CategoryMapping"
|
|
@import.mappings.create! key: "TestCategory2", mappable: categories(:food_and_drink), type: "Import::CategoryMapping"
|
|
@import.mappings.create! key: "", create_when_empty: false, mappable: nil, type: "Import::CategoryMapping" # Leaves uncategorized
|
|
|
|
@import.mappings.create! key: "TestTag1", create_when_empty: true, type: "Import::TagMapping"
|
|
@import.mappings.create! key: "TestTag2", mappable: tags(:one), type: "Import::TagMapping"
|
|
@import.mappings.create! key: "", create_when_empty: false, mappable: nil, type: "Import::TagMapping" # Leaves untagged
|
|
|
|
@import.mappings.create! key: "TestAccount1", create_when_empty: true, type: "Import::AccountMapping"
|
|
@import.mappings.create! key: "TestAccount2", mappable: accounts(:depository), type: "Import::AccountMapping"
|
|
@import.mappings.create! key: "", mappable: accounts(:depository), type: "Import::AccountMapping"
|
|
|
|
@import.reload
|
|
|
|
assert_difference -> { Entry.count } => 3,
|
|
-> { Transaction.count } => 3,
|
|
-> { Tag.count } => 1,
|
|
-> { Category.count } => 1,
|
|
-> { Account.count } => 1 do
|
|
@import.publish
|
|
end
|
|
|
|
assert_equal "complete", @import.status
|
|
end
|
|
|
|
test "imports transactions with separate type column for signage convention" do
|
|
import = <<~CSV
|
|
date,amount,amount_type
|
|
01/01/2024,100,debit
|
|
01/02/2024,200,credit
|
|
01/03/2024,300,debit
|
|
CSV
|
|
|
|
@import.update!(
|
|
account: accounts(:depository),
|
|
raw_file_str: import,
|
|
date_col_label: "date",
|
|
date_format: "%m/%d/%Y",
|
|
amount_col_label: "amount",
|
|
entity_type_col_label: "amount_type",
|
|
amount_type_inflow_value: "debit",
|
|
amount_type_strategy: "custom_column",
|
|
signage_convention: nil # Explicitly set to nil to prove this is not needed
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
|
|
@import.reload
|
|
|
|
assert_difference -> { Entry.count } => 3,
|
|
-> { Transaction.count } => 3 do
|
|
@import.publish
|
|
end
|
|
|
|
assert_equal [ -100, 200, -300 ], @import.entries.map(&:amount)
|
|
end
|
|
|
|
test "does not create duplicate when matching transaction exists with same name" do
|
|
account = accounts(:depository)
|
|
|
|
# Create an existing manual transaction
|
|
existing_entry = account.entries.create!(
|
|
date: Date.new(2024, 1, 1),
|
|
amount: 100,
|
|
currency: "USD",
|
|
name: "Coffee Shop",
|
|
entryable: Transaction.new(category: categories(:food_and_drink))
|
|
)
|
|
|
|
# Try to import a CSV with the same transaction
|
|
import_csv = <<~CSV
|
|
date,name,amount
|
|
01/01/2024,Coffee Shop,100
|
|
CSV
|
|
|
|
@import.update!(
|
|
account: account,
|
|
raw_file_str: import_csv,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
date_format: "%m/%d/%Y",
|
|
amount_type_strategy: "signed_amount",
|
|
signage_convention: "inflows_negative"
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
@import.reload
|
|
|
|
# Should not create a new entry, should update the existing one
|
|
assert_no_difference -> { Entry.count } do
|
|
assert_no_difference -> { Transaction.count } do
|
|
@import.publish
|
|
end
|
|
end
|
|
|
|
# The existing entry should now be linked to the import
|
|
assert_equal @import.id, existing_entry.reload.import_id
|
|
end
|
|
|
|
test "creates new transaction when name differs even if date and amount match" do
|
|
account = accounts(:depository)
|
|
|
|
# Create an existing manual transaction
|
|
existing_entry = account.entries.create!(
|
|
date: Date.new(2024, 1, 1),
|
|
amount: 100,
|
|
currency: "USD",
|
|
name: "Coffee Shop",
|
|
entryable: Transaction.new
|
|
)
|
|
|
|
# Try to import a CSV with same date/amount but different name
|
|
import_csv = <<~CSV
|
|
date,name,amount
|
|
01/01/2024,Different Store,100
|
|
CSV
|
|
|
|
@import.update!(
|
|
account: account,
|
|
raw_file_str: import_csv,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
date_format: "%m/%d/%Y",
|
|
amount_type_strategy: "signed_amount",
|
|
signage_convention: "inflows_negative"
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
@import.reload
|
|
|
|
# Should create a new entry because the name is different
|
|
assert_difference -> { Entry.count } => 1,
|
|
-> { Transaction.count } => 1 do
|
|
@import.publish
|
|
end
|
|
|
|
# Both transactions should exist
|
|
assert_equal 2, account.entries.where(date: Date.new(2024, 1, 1), amount: 100).count
|
|
end
|
|
|
|
test "imports all identical transactions from CSV even when one exists in database" do
|
|
account = accounts(:depository)
|
|
|
|
# Create an existing manual transaction
|
|
existing_entry = account.entries.create!(
|
|
date: Date.new(2024, 1, 1),
|
|
amount: 50,
|
|
currency: "USD",
|
|
name: "Vending Machine",
|
|
entryable: Transaction.new
|
|
)
|
|
|
|
# Import CSV with 3 identical transactions (e.g., buying from vending machine 3 times)
|
|
import_csv = <<~CSV
|
|
date,name,amount
|
|
01/01/2024,Vending Machine,50
|
|
01/01/2024,Vending Machine,50
|
|
01/01/2024,Vending Machine,50
|
|
CSV
|
|
|
|
@import.update!(
|
|
account: account,
|
|
raw_file_str: import_csv,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
date_format: "%m/%d/%Y",
|
|
amount_type_strategy: "signed_amount",
|
|
signage_convention: "inflows_negative"
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
@import.reload
|
|
|
|
# Should update 1 existing and create 2 new (total of 3 in system)
|
|
# The first matching row claims the existing entry, the other 2 create new ones
|
|
assert_difference -> { Entry.count } => 2,
|
|
-> { Transaction.count } => 2 do
|
|
@import.publish
|
|
end
|
|
|
|
# Should have exactly 3 identical transactions total
|
|
assert_equal 3, account.entries.where(
|
|
date: Date.new(2024, 1, 1),
|
|
amount: 50,
|
|
name: "Vending Machine"
|
|
).count
|
|
|
|
# The existing entry should be linked to the import
|
|
assert_equal @import.id, existing_entry.reload.import_id
|
|
end
|
|
|
|
test "imports all identical transactions from CSV when none exist in database" do
|
|
account = accounts(:depository)
|
|
|
|
# Import CSV with 3 identical transactions (no existing entry in database)
|
|
import_csv = <<~CSV
|
|
date,name,amount
|
|
01/01/2024,Vending Machine,50
|
|
01/01/2024,Vending Machine,50
|
|
01/01/2024,Vending Machine,50
|
|
CSV
|
|
|
|
@import.update!(
|
|
account: account,
|
|
raw_file_str: import_csv,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
date_format: "%m/%d/%Y",
|
|
amount_type_strategy: "signed_amount",
|
|
signage_convention: "inflows_negative"
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
@import.reload
|
|
|
|
# Should create all 3 as new transactions
|
|
assert_difference -> { Entry.count } => 3,
|
|
-> { Transaction.count } => 3 do
|
|
@import.publish
|
|
end
|
|
|
|
# Should have exactly 3 identical transactions
|
|
assert_equal 3, account.entries.where(
|
|
date: Date.new(2024, 1, 1),
|
|
amount: 50,
|
|
name: "Vending Machine"
|
|
).count
|
|
end
|
|
|
|
test "uses family currency as fallback when account has no currency and no CSV currency column" do
|
|
account = accounts(:depository)
|
|
family = account.family
|
|
|
|
# Clear the account's currency to simulate an account without currency set
|
|
account.update_column(:currency, nil)
|
|
|
|
import_csv = <<~CSV
|
|
date,name,amount
|
|
01/01/2024,Test Transaction,100
|
|
CSV
|
|
|
|
@import.update!(
|
|
account: account,
|
|
raw_file_str: import_csv,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
date_format: "%m/%d/%Y",
|
|
amount_type_strategy: "signed_amount",
|
|
signage_convention: "inflows_negative"
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
@import.reload
|
|
|
|
assert_difference -> { Entry.count } => 1 do
|
|
@import.publish
|
|
end
|
|
|
|
# The transaction should have the family's currency as fallback
|
|
entry = @import.entries.first
|
|
assert_equal family.currency, entry.currency
|
|
end
|
|
|
|
test "does not raise error when all accounts are properly mapped" do
|
|
# Import CSV with multiple accounts, all mapped
|
|
import_csv = <<~CSV
|
|
date,name,amount,account
|
|
01/01/2024,Coffee Shop,100,Checking Account
|
|
01/02/2024,Grocery Store,200,Credit Card
|
|
CSV
|
|
|
|
checking = accounts(:depository)
|
|
credit_card = accounts(:credit_card)
|
|
|
|
@import.update!(
|
|
account: nil,
|
|
raw_file_str: import_csv,
|
|
date_col_label: "date",
|
|
amount_col_label: "amount",
|
|
name_col_label: "name",
|
|
account_col_label: "account",
|
|
date_format: "%m/%d/%Y",
|
|
amount_type_strategy: "signed_amount",
|
|
signage_convention: "inflows_negative"
|
|
)
|
|
|
|
@import.generate_rows_from_csv
|
|
|
|
# Map both accounts
|
|
@import.mappings.create!(key: "Checking Account", mappable: checking, type: "Import::AccountMapping")
|
|
@import.mappings.create!(key: "Credit Card", mappable: credit_card, type: "Import::AccountMapping")
|
|
@import.mappings.create!(key: "", mappable: nil, create_when_empty: false, type: "Import::CategoryMapping")
|
|
@import.mappings.create!(key: "", mappable: nil, create_when_empty: false, type: "Import::TagMapping")
|
|
|
|
@import.reload
|
|
|
|
# Should succeed without errors
|
|
assert_difference -> { Entry.count } => 2,
|
|
-> { Transaction.count } => 2 do
|
|
@import.publish
|
|
end
|
|
|
|
assert_equal "complete", @import.status
|
|
|
|
# Check that each account got one entry from this import
|
|
assert_equal 1, checking.entries.where(import: @import).count
|
|
assert_equal 1, credit_card.entries.where(import: @import).count
|
|
end
|
|
end
|