mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-21 22:29:38 +02:00
feat(import): add currency and number format support for CSV imports (#1819)
* feat(import): add currency and number format support for CSV imports * feat(import): add currency and format for mint and trade * fix(imports): remove currency field in favor of currency csv col * fix(imports): remove validate column from import model * test(import): add some tests for imports * test(import): add some tests for generate_rows_from_csv * fix: change method name for import model * fix: change before validation --------- Co-authored-by: danestves <danestves@users.noreply.github.com>
This commit is contained in:
parent
9e5f1574bc
commit
077694bbde
9 changed files with 307 additions and 13 deletions
|
@ -2,8 +2,17 @@ class Import < ApplicationRecord
|
||||||
TYPES = %w[TransactionImport TradeImport AccountImport MintImport].freeze
|
TYPES = %w[TransactionImport TradeImport AccountImport MintImport].freeze
|
||||||
SIGNAGE_CONVENTIONS = %w[inflows_positive inflows_negative]
|
SIGNAGE_CONVENTIONS = %w[inflows_positive inflows_negative]
|
||||||
|
|
||||||
|
NUMBER_FORMATS = {
|
||||||
|
"1,234.56" => { separator: ".", delimiter: "," }, # US/UK/Asia
|
||||||
|
"1.234,56" => { separator: ",", delimiter: "." }, # Most of Europe
|
||||||
|
"1 234,56" => { separator: ",", delimiter: " " }, # French/Scandinavian
|
||||||
|
"1,234" => { separator: "", delimiter: "," } # Zero-decimal currencies like JPY
|
||||||
|
}.freeze
|
||||||
|
|
||||||
belongs_to :family
|
belongs_to :family
|
||||||
|
|
||||||
|
before_validation :set_default_number_format
|
||||||
|
|
||||||
scope :ordered, -> { order(created_at: :desc) }
|
scope :ordered, -> { order(created_at: :desc) }
|
||||||
|
|
||||||
enum :status, {
|
enum :status, {
|
||||||
|
@ -18,6 +27,7 @@ class Import < ApplicationRecord
|
||||||
validates :type, inclusion: { in: TYPES }
|
validates :type, inclusion: { in: TYPES }
|
||||||
validates :col_sep, inclusion: { in: [ ",", ";" ] }
|
validates :col_sep, inclusion: { in: [ ",", ";" ] }
|
||||||
validates :signage_convention, inclusion: { in: SIGNAGE_CONVENTIONS }
|
validates :signage_convention, inclusion: { in: SIGNAGE_CONVENTIONS }
|
||||||
|
validates :number_format, presence: true, inclusion: { in: NUMBER_FORMATS.keys }
|
||||||
|
|
||||||
has_many :rows, dependent: :destroy
|
has_many :rows, dependent: :destroy
|
||||||
has_many :mappings, dependent: :destroy
|
has_many :mappings, dependent: :destroy
|
||||||
|
@ -95,8 +105,8 @@ class Import < ApplicationRecord
|
||||||
def generate_rows_from_csv
|
def generate_rows_from_csv
|
||||||
rows.destroy_all
|
rows.destroy_all
|
||||||
|
|
||||||
mapped_rows = csv_rows.map do |row|
|
csv_rows.each do |row|
|
||||||
{
|
rows.create!(
|
||||||
account: row[account_col_label].to_s,
|
account: row[account_col_label].to_s,
|
||||||
date: row[date_col_label].to_s,
|
date: row[date_col_label].to_s,
|
||||||
qty: sanitize_number(row[qty_col_label]).to_s,
|
qty: sanitize_number(row[qty_col_label]).to_s,
|
||||||
|
@ -109,10 +119,8 @@ class Import < ApplicationRecord
|
||||||
tags: row[tags_col_label].to_s,
|
tags: row[tags_col_label].to_s,
|
||||||
entity_type: row[entity_type_col_label].to_s,
|
entity_type: row[entity_type_col_label].to_s,
|
||||||
notes: row[notes_col_label].to_s
|
notes: row[notes_col_label].to_s
|
||||||
}
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
rows.insert_all!(mapped_rows)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_mappings
|
def sync_mappings
|
||||||
|
@ -177,6 +185,39 @@ class Import < ApplicationRecord
|
||||||
|
|
||||||
def sanitize_number(value)
|
def sanitize_number(value)
|
||||||
return "" if value.nil?
|
return "" if value.nil?
|
||||||
value.gsub(/[^\d.\-]/, "")
|
|
||||||
|
format = NUMBER_FORMATS[number_format]
|
||||||
|
return "" unless format
|
||||||
|
|
||||||
|
# First, normalize spaces and remove any characters that aren't numbers, delimiters, separators, or minus signs
|
||||||
|
sanitized = value.to_s.strip
|
||||||
|
|
||||||
|
# Handle French/Scandinavian format specially
|
||||||
|
if format[:delimiter] == " "
|
||||||
|
sanitized = sanitized.gsub(/\s+/, "") # Remove all spaces first
|
||||||
|
else
|
||||||
|
sanitized = sanitized.gsub(/[^\d#{Regexp.escape(format[:delimiter])}#{Regexp.escape(format[:separator])}\-]/, "")
|
||||||
|
|
||||||
|
# Replace delimiter with empty string
|
||||||
|
if format[:delimiter].present?
|
||||||
|
sanitized = sanitized.gsub(format[:delimiter], "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Replace separator with period for proper float parsing
|
||||||
|
if format[:separator].present?
|
||||||
|
sanitized = sanitized.gsub(format[:separator], ".")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return empty string if not a valid number
|
||||||
|
unless sanitized =~ /\A-?\d+\.?\d*\z/
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
sanitized
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_default_number_format
|
||||||
|
self.number_format ||= "1,234.56" # Default to US/UK format
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,10 @@
|
||||||
<%= form.select :signage_convention, [["Incomes are negative", "inflows_negative"], ["Incomes are positive", "inflows_positive"]], { label: true }, disabled: import.complete? %>
|
<%= form.select :signage_convention, [["Incomes are negative", "inflows_negative"], ["Incomes are positive", "inflows_positive"]], { label: true }, disabled: import.complete? %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" }, disabled: import.complete? %>
|
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" }, disabled: import.complete? %>
|
||||||
|
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" }, disabled: import.complete? %>
|
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" }, disabled: import.complete? %>
|
||||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" }, disabled: import.complete? %>
|
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" }, disabled: import.complete? %>
|
||||||
|
|
|
@ -11,7 +11,10 @@
|
||||||
<%= form.select :signage_convention, [["Buys are positive qty", "inflows_positive"], ["Buys are negative qty", "inflows_negative"]], label: true %>
|
<%= form.select :signage_convention, [["Buys are positive qty", "inflows_positive"], ["Buys are negative qty", "inflows_negative"]], label: true %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" } %>
|
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" } %>
|
||||||
|
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%= form.select :ticker_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Ticker" } %>
|
<%= form.select :ticker_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Ticker" } %>
|
||||||
<%= form.select :price_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Price" } %>
|
<%= form.select :price_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Price" } %>
|
||||||
|
|
|
@ -11,7 +11,10 @@
|
||||||
<%= form.select :signage_convention, [["Incomes are positive", "inflows_positive"], ["Incomes are negative", "inflows_negative"]], label: true %>
|
<%= form.select :signage_convention, [["Incomes are positive", "inflows_positive"], ["Incomes are negative", "inflows_negative"]], label: true %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" } %>
|
<%= form.select :currency_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Currency" } %>
|
||||||
|
<%= form.select :number_format, Import::NUMBER_FORMATS.keys, { label: "Format", prompt: "Select format" }, required: true %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
<%= form.select :account_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Account (optional)" } %>
|
||||||
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
<%= form.select :name_col_label, import.csv_headers, { include_blank: "Leave empty", label: "Name (optional)" } %>
|
||||||
|
|
|
@ -7,3 +7,7 @@ en:
|
||||||
attributes:
|
attributes:
|
||||||
raw_file_str:
|
raw_file_str:
|
||||||
invalid_csv_format: is not a valid CSV format
|
invalid_csv_format: is not a valid CSV format
|
||||||
|
attributes:
|
||||||
|
import:
|
||||||
|
currency: Currency
|
||||||
|
number_format: Number Format
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddNumberFormatToImports < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :imports, :number_format, :string
|
||||||
|
end
|
||||||
|
end
|
5
db/schema.rb
generated
5
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2025_02_06_204404) do
|
ActiveRecord::Schema[7.2].define(version: 2025_02_07_014022) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -101,7 +101,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_06_204404) do
|
||||||
t.decimal "balance", precision: 19, scale: 4
|
t.decimal "balance", precision: 19, scale: 4
|
||||||
t.string "currency"
|
t.string "currency"
|
||||||
t.boolean "is_active", default: true, null: false
|
t.boolean "is_active", default: true, null: false
|
||||||
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
|
||||||
t.uuid "import_id"
|
t.uuid "import_id"
|
||||||
t.uuid "plaid_account_id"
|
t.uuid "plaid_account_id"
|
||||||
t.boolean "scheduled_for_deletion", default: false
|
t.boolean "scheduled_for_deletion", default: false
|
||||||
|
@ -414,6 +414,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_02_06_204404) do
|
||||||
t.string "date_format", default: "%m/%d/%Y"
|
t.string "date_format", default: "%m/%d/%Y"
|
||||||
t.string "signage_convention", default: "inflows_positive"
|
t.string "signage_convention", default: "inflows_positive"
|
||||||
t.string "error"
|
t.string "error"
|
||||||
|
t.string "number_format"
|
||||||
t.index ["family_id"], name: "index_imports_on_family_id"
|
t.index ["family_id"], name: "index_imports_on_family_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -54,4 +54,233 @@ module ImportInterfaceTest
|
||||||
assert_equal "Failed to publish", import.error
|
assert_equal "Failed to publish", import.error
|
||||||
assert_equal "failed", import.status
|
assert_equal "failed", import.status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "parses US/UK number format correctly" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
number_format: "1,234.56",
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_data = "date,amount,name\n01/01/2024,\"1,234.56\",Test"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "1234.56", row.amount
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses European number format correctly" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
number_format: "1.234,56",
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_data = "date,amount,name\n01/01/2024,\"1.234,56\",Test"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "1234.56", row.amount
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses French/Scandinavian number format correctly" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
number_format: "1 234,56",
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quote the amount field to ensure proper CSV parsing
|
||||||
|
csv_data = "date,amount,name\n01/01/2024,\"1 234,56\",Test"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "1234.56", row.amount
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses zero-decimal currency format correctly" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
number_format: "1,234",
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_data = "date,amount,name\n01/01/2024,1234,Test"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "1234", row.amount
|
||||||
|
end
|
||||||
|
|
||||||
|
test "currency from CSV takes precedence over default" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
currency_col_label: "currency",
|
||||||
|
number_format: "1,234.56",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
import.family.update!(currency: "USD")
|
||||||
|
|
||||||
|
csv_data = "date,amount,name,currency\n01/01/2024,123.45,Test,EUR"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "EUR", row.currency
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses default currency when CSV currency column is empty" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
currency_col_label: "currency",
|
||||||
|
number_format: "1,234.56",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
import.family.update!(currency: "USD")
|
||||||
|
|
||||||
|
csv_data = "date,amount,name,currency\n01/01/2024,123.45,Test,"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "USD", row.currency
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses default currency when CSV has no currency column" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
number_format: "1,234.56",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
import.family.update!(currency: "USD")
|
||||||
|
|
||||||
|
csv_data = "date,amount,name\n01/01/2024,123.45,Test"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "USD", row.currency
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates rows with all optional fields" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
account_col_label: "account",
|
||||||
|
category_col_label: "category",
|
||||||
|
tags_col_label: "tags",
|
||||||
|
notes_col_label: "notes",
|
||||||
|
currency_col_label: "currency",
|
||||||
|
number_format: "1,234.56",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_data = "date,amount,name,account,category,tags,notes,currency\n" \
|
||||||
|
"01/01/2024,1234.56,Salary,Bank Account,Income,\"monthly,salary\",Salary payment,EUR"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "01/01/2024", row.date
|
||||||
|
assert_equal "1234.56", row.amount
|
||||||
|
assert_equal "Salary", row.name
|
||||||
|
assert_equal "Bank Account", row.account
|
||||||
|
assert_equal "Income", row.category
|
||||||
|
assert_equal "monthly,salary", row.tags
|
||||||
|
assert_equal "Salary payment", row.notes
|
||||||
|
assert_equal "EUR", row.currency
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generates rows with minimal required fields" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
number_format: "1,234.56",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_data = "date,amount\n01/01/2024,1234.56"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "01/01/2024", row.date
|
||||||
|
assert_equal "1234.56", row.amount
|
||||||
|
assert_equal "Imported item", row.name # Default name
|
||||||
|
assert_equal import.family.currency, row.currency # Default currency
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles empty values in optional fields" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
name_col_label: "name",
|
||||||
|
category_col_label: "category",
|
||||||
|
tags_col_label: "tags",
|
||||||
|
number_format: "1,234.56",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_data = "date,amount,name,category,tags\n01/01/2024,1234.56,,,"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "01/01/2024", row.date
|
||||||
|
assert_equal "1234.56", row.amount
|
||||||
|
assert_equal "Imported item", row.name # Falls back to default
|
||||||
|
assert_equal "", row.category
|
||||||
|
assert_equal "", row.tags
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles trade-specific fields" do
|
||||||
|
import = imports(:transaction)
|
||||||
|
import.update!(
|
||||||
|
amount_col_label: "amount",
|
||||||
|
date_col_label: "date",
|
||||||
|
qty_col_label: "quantity",
|
||||||
|
ticker_col_label: "symbol",
|
||||||
|
price_col_label: "price",
|
||||||
|
number_format: "1,234.56",
|
||||||
|
date_format: "%m/%d/%Y"
|
||||||
|
)
|
||||||
|
|
||||||
|
csv_data = "date,amount,quantity,symbol,price\n01/01/2024,1234.56,10,AAPL,123.456"
|
||||||
|
import.update!(raw_file_str: csv_data)
|
||||||
|
import.generate_rows_from_csv
|
||||||
|
|
||||||
|
row = import.rows.first
|
||||||
|
assert_equal "10", row.qty
|
||||||
|
assert_equal "AAPL", row.ticker
|
||||||
|
assert_equal "123.456", row.price
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,14 +28,19 @@ class TransactionImportTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test "imports transactions, categories, tags, and accounts" do
|
test "imports transactions, categories, tags, and accounts" do
|
||||||
import = <<-CSV
|
import = <<~CSV
|
||||||
date,name,amount,category,tags,account,notes
|
date,name,amount,category,tags,account,notes
|
||||||
01/01/2024,Txn1,100,TestCategory1,TestTag1,TestAccount1,notes1
|
01/01/2024,Txn1,100,TestCategory1,TestTag1,TestAccount1,notes1
|
||||||
01/02/2024,Txn2,200,TestCategory2,TestTag1|TestTag2,TestAccount2,notes2
|
01/02/2024,Txn2,200,TestCategory2,TestTag1|TestTag2,TestAccount2,notes2
|
||||||
01/03/2024,Txn3,300,,,,notes3
|
01/03/2024,Txn3,300,,,,notes3
|
||||||
CSV
|
CSV
|
||||||
|
|
||||||
@import.update!(raw_file_str: import)
|
@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.generate_rows_from_csv
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue