Finance-Tiller2QIF

 view release on metacpan or  search on metacpan

t/interactive.t  view on Meta::CPAN

use 5.036;
use utf8;
use warnings FATAL => 'utf8';
use open ':std', ':encoding(UTF-8)';
use Test2::V0;
use Capture::Tiny qw(capture);
use File::Temp qw(tempdir);
use Path::Tiny;
use Finance::Tiller2QIF;
use Finance::Tiller2QIF::Interactive;
use Finance::Tiller2QIF::Util;
use Finance::Tiller2QIF::DB qw(connect_db);

sub fixture () {
  my $dir = tempdir(CLEANUP => 1);
  # Match Run's canonical Windows paths so failure-injection targets compare
  # equal despite File::Temp returning backslash-separated directory names.
  $dir = path($dir)->realpath->stringify if $^O eq 'MSWin32';
  my $f = { dir => $dir, db => "$dir/live.sqlite3", input => "$dir/input.csv",
    output => "$dir/result.qif", mapfile => "$dir/rules.map" };
  Finance::Tiller2QIF::Util::InitDB($f->{db});
  path($f->{input})->spew_utf8(
    "Date,Transaction ID,Account,Amount,Description,Full Description,Category\n"
    . "04/25/2026,tx-α-123456789,Checking,100.00,Deposit,Paycheck,Income\n");
  path($f->{mapfile})->spew_utf8("category | Income | Income:Salary\n");
  path($f->{output})->spew_utf8("existing QIF\n");
  $f->{original} = Finance::Tiller2QIF::Interactive::_digest($f->{db});
  return $f;
}

sub unchanged ($f) {
  is(Finance::Tiller2QIF::Interactive::_digest($f->{db}), $f->{original},
    'original database bytes unchanged');
  is(path($f->{output})->slurp_utf8, "existing QIF\n", 'existing QIF unchanged');
}

sub run_session ($f, $answers, $edit=undef, @extra) {
  my $views = 0;
  my $view = \&Finance::Tiller2QIF::Interactive::_view;
  no warnings 'redefine';
  local *Finance::Tiller2QIF::Interactive::_view = sub ($session) {
    $f->{session} = $session;
    $edit->($session, ++$views) if $edit;
    $view->($session);
  };
  local @ARGV = ('interactive', '--db', $f->{db}, '--input', $f->{input},
    '--output', $f->{output},
    (defined $f->{mapfile} ? ('--mapfile', $f->{mapfile}) : ()), @extra);
  open my $input, '<', \$answers or die $!;
  local *STDIN = $input;
  my ($result, $error);
  my ($out, $err) = capture {
    $result = eval { Finance::Tiller2QIF::run_cli() };
    $error = $@;
  };
  return { out => $out, err => $err, error => $error, result => $result };
}

# These cases hit intermittent Capture::Tiny tempfile errors on one Windows
# tester. Keep them running as TODO until another tester reports the issue.
sub capture_subtest ($name, $code) {
  my $todo = todo('Intermittent Capture::Tiny tempfile permission failure on Windows')
    if $^O eq 'MSWin32';
  subtest $name => $code;
}

subtest 'cancel and EOF keep originals, retain artifacts' => sub {
  for my $answer ("c\n", '') {
    my $f = fixture();
    my $r = run_session($f, $answer);
    is($r->{error}, '', 'session returns normally');
    is($r->{result}, 0, 'cancel is unsuccessful');
    unchanged($f);
    ok(-f "$f->{session}{dir}/corrections.t2qsql", 'SQL retained with dedicated editor extension');
    ok(-f $f->{session}{review}{db}, 'working database retained');
    like($r->{out}, qr/ID: tx-α-123456789/, 'full transaction ID displayed');
  }
};

subtest 'refresh reconsiders skips, replays hooks once, and preserves overrides' => sub {
  my $f = fixture();
  path($f->{mapfile})->spew_utf8("default | skip\n");
  path("$f->{dir}/before.sql")->spew_utf8("UPDATE transactions SET amount = amount + 2;\n");
  path("$f->{dir}/after.sql")->spew_utf8("UPDATE transactions SET amount = amount + 3;\n");
  my $r = run_session($f, "e\na\na\ne\n", sub ($s, $view) {
    unchanged($f);
    if ($view == 1) {
      is($s->{review}{count}, 0, 'initial mapping skips transaction');
      path($f->{mapfile})->spew_utf8("default | Income:Updated\n");
      path($s->{options}{corrections})->spew_utf8(
        "UPDATE transactions SET amount = amount + 1; UPDATE transactions SET mapped_category = 'Income:Override', memo = 'one; two';\n");
    }
  }, '--beforemap', "$f->{dir}/before.sql", '--aftermap', "$f->{dir}/after.sql");
  is($r->{error}, '', 'export succeeded');
  is($r->{result}, 1, 'session committed');
  like($r->{out}, qr/Apply and refresh successfully/, 'stale export refused');
  my $dbh = connect_db($f->{db});
  is($dbh->selectrow_arrayref('SELECT amount, mapped_category, exported, skipped FROM transactions'),
    [106, 'Income:Override', 1, 0], 'fresh replay includes previously skipped row and one-off override');
  $dbh->disconnect;
  like(path($f->{output})->slurp_utf8, qr/T106\.00.*LIncome:Override/s, 'QIF matches reviewed data');
  ok(!-e "$f->{db}.interactive.json", 'publication journal cleaned');
};

subtest 'failed SQL cannot publish partial changes and can be repaired' => sub {
  my $f = fixture();



( run in 0.381 second using v1.01-cache-2.11-cpan-194103478d8 )