mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
* feat(config): add Lunchflow runtime configuration flags * feat(api): add include_pending parameter to Lunchflow API * feat(processor): add pending metadata support to Lunchflow processor * feat(processor): generate temporary IDs for pending transactions * feat(importer): integrate pending transaction support in sync * fix(importer): improve deduplication for transactions without IDs * feat(model): add Lunchflow pending support to Transaction scopes * test: add Lunchflow processor pending metadata tests * docs: update AGENTS.md for Lunchflow pending support * chore: remove unused variable * fix: simplify key check * fix: dotenv-linter key order * fix: avoid collapsing distinct pending transactions * fix: prevent unbounded raw payload growth for blank IDs
203 lines
5.8 KiB
Ruby
203 lines
5.8 KiB
Ruby
require "test_helper"
|
|
|
|
class LunchflowEntry::ProcessorTest < ActiveSupport::TestCase
|
|
setup do
|
|
@family = families(:empty)
|
|
@lunchflow_item = LunchflowItem.create!(
|
|
name: "Test Lunchflow Connection",
|
|
api_key: "test_key",
|
|
family: @family
|
|
)
|
|
@lunchflow_account = LunchflowAccount.create!(
|
|
lunchflow_item: @lunchflow_item,
|
|
name: "Test Account",
|
|
currency: "USD",
|
|
account_id: "lf_acc_123"
|
|
)
|
|
|
|
# Create a real account and link it
|
|
@account = Account.create!(
|
|
family: @family,
|
|
name: "Test Checking",
|
|
accountable: Depository.new(subtype: "checking"),
|
|
balance: 1000,
|
|
currency: "USD"
|
|
)
|
|
|
|
AccountProvider.create!(
|
|
account: @account,
|
|
provider: @lunchflow_account
|
|
)
|
|
|
|
@lunchflow_account.reload
|
|
end
|
|
|
|
test "stores pending metadata when isPending is true" do
|
|
transaction_data = {
|
|
id: "lf_txn_123",
|
|
accountId: 456,
|
|
amount: -50.00,
|
|
currency: "USD",
|
|
date: "2025-01-15",
|
|
merchant: "Test Merchant",
|
|
description: "Test transaction",
|
|
isPending: true
|
|
}
|
|
|
|
result = LunchflowEntry::Processor.new(
|
|
transaction_data,
|
|
lunchflow_account: @lunchflow_account
|
|
).process
|
|
|
|
assert_not_nil result
|
|
transaction = result.entryable
|
|
assert_kind_of Transaction, transaction
|
|
assert_equal true, transaction.pending?
|
|
assert_equal true, transaction.extra.dig("lunchflow", "pending")
|
|
end
|
|
|
|
test "stores pending false when isPending is false" do
|
|
transaction_data = {
|
|
id: "lf_txn_124",
|
|
accountId: 456,
|
|
amount: -50.00,
|
|
currency: "USD",
|
|
date: "2025-01-15",
|
|
merchant: "Test Merchant",
|
|
description: "Test transaction",
|
|
isPending: false
|
|
}
|
|
|
|
result = LunchflowEntry::Processor.new(
|
|
transaction_data,
|
|
lunchflow_account: @lunchflow_account
|
|
).process
|
|
|
|
assert_not_nil result
|
|
transaction = result.entryable
|
|
assert_equal false, transaction.pending?
|
|
assert_equal false, transaction.extra.dig("lunchflow", "pending")
|
|
end
|
|
|
|
test "does not store pending metadata when isPending is absent" do
|
|
transaction_data = {
|
|
id: "lf_txn_125",
|
|
accountId: 456,
|
|
amount: -50.00,
|
|
currency: "USD",
|
|
date: "2025-01-15",
|
|
merchant: "Test Merchant",
|
|
description: "Test transaction"
|
|
}
|
|
|
|
result = LunchflowEntry::Processor.new(
|
|
transaction_data,
|
|
lunchflow_account: @lunchflow_account
|
|
).process
|
|
|
|
assert_not_nil result
|
|
transaction = result.entryable
|
|
assert_not transaction.pending?
|
|
assert_nil transaction.extra.dig("lunchflow", "pending")
|
|
assert_nil transaction.extra.dig("lunchflow")
|
|
end
|
|
|
|
test "handles string true value for isPending" do
|
|
transaction_data = {
|
|
id: "lf_txn_126",
|
|
accountId: 456,
|
|
amount: -50.00,
|
|
currency: "USD",
|
|
date: "2025-01-15",
|
|
merchant: "Test Merchant",
|
|
description: "Test transaction",
|
|
isPending: "true"
|
|
}
|
|
|
|
result = LunchflowEntry::Processor.new(
|
|
transaction_data,
|
|
lunchflow_account: @lunchflow_account
|
|
).process
|
|
|
|
assert_not_nil result
|
|
transaction = result.entryable
|
|
assert_equal true, transaction.pending?
|
|
assert_equal true, transaction.extra.dig("lunchflow", "pending")
|
|
end
|
|
|
|
test "generates temporary ID for pending transactions with blank ID" do
|
|
transaction_data = {
|
|
id: "",
|
|
accountId: 456,
|
|
amount: -50.00,
|
|
currency: "USD",
|
|
date: "2025-01-15",
|
|
merchant: "Test Merchant",
|
|
description: "Pending transaction",
|
|
isPending: true
|
|
}
|
|
|
|
# Process transaction with blank ID
|
|
result = LunchflowEntry::Processor.new(
|
|
transaction_data,
|
|
lunchflow_account: @lunchflow_account
|
|
).process
|
|
|
|
assert_not_nil result
|
|
transaction = result.entryable
|
|
assert_kind_of Transaction, transaction
|
|
assert transaction.pending?
|
|
|
|
# Verify the entry has a generated external_id (since we can't have blank IDs)
|
|
assert result.external_id.present?
|
|
assert_match /^lunchflow_pending_[a-f0-9]{32}$/, result.external_id
|
|
|
|
# Note: Calling the processor again with identical data will trigger collision
|
|
# detection and create a SECOND entry (with _1 suffix). In real syncs, the
|
|
# importer's deduplication prevents this. For true idempotency testing,
|
|
# use the importer, not the processor directly.
|
|
end
|
|
|
|
test "generates unique IDs for multiple pending transactions with identical attributes" do
|
|
# Two pending transactions with same merchant, amount, date (e.g., two Uber rides)
|
|
transaction_data = {
|
|
id: "",
|
|
accountId: 456,
|
|
amount: -15.00,
|
|
currency: "USD",
|
|
date: "2025-01-15",
|
|
merchant: "UBER",
|
|
description: "Ride",
|
|
isPending: true
|
|
}
|
|
|
|
# Process first transaction
|
|
result1 = LunchflowEntry::Processor.new(
|
|
transaction_data,
|
|
lunchflow_account: @lunchflow_account
|
|
).process
|
|
|
|
assert_not_nil result1
|
|
assert_match /^lunchflow_pending_[a-f0-9]{32}$/, result1.external_id
|
|
|
|
# Process second transaction with IDENTICAL attributes
|
|
result2 = LunchflowEntry::Processor.new(
|
|
transaction_data,
|
|
lunchflow_account: @lunchflow_account
|
|
).process
|
|
|
|
assert_not_nil result2
|
|
|
|
# Should create a DIFFERENT entry (not update the first one)
|
|
assert_not_equal result1.id, result2.id, "Should create separate entries for distinct pending transactions"
|
|
|
|
# Second should have a counter appended to avoid collision
|
|
assert_match /^lunchflow_pending_[a-f0-9]{32}_\d+$/, result2.external_id
|
|
assert_not_equal result1.external_id, result2.external_id, "Should generate different external_ids to avoid collision"
|
|
|
|
# Verify both transactions exist
|
|
entries = @account.entries.where(source: "lunchflow", "entries.date": "2025-01-15")
|
|
assert_equal 2, entries.count, "Should have created 2 separate entries"
|
|
end
|
|
end
|