revbank/plugins/users
Juerd Waalboer 52749df5f3 Ignore all hook exceptions except in hook_checkout_prepare
A space had a custom plugin that died during hook_checkout, which caused
the CHECKOUT lines to be logged without the corresponding BALANCE, and
indeed no account balances were updated. While the plugin had a bug, it
should not cause a half transaction in RevBank.

After some hesitation, I went with ON ERROR RESUME NEXT because if a
hook throws an exception, that should not interfere with other plugins
(the hook can return ABORT if this it was intentional), including the
calling plugin. An error message is printed (but not logged... TODO: add
hook_plugin_fail to plugins/log) but the show must go on.

During hook_checkout_prepare, however, nothing is set in stone yet, so
this could be used for something that might die, and this instance of
call_hooks() is now the one place where a failing hook should result in
the transaction getting aborted. For this, call_hooks() now returns a
success status boolean. Maybe it would make sense in more places, but I
didn't identify any such calls yet.

RevBank::Cart->checkout used to return a success status boolean, but it
could just as well just die (indirectly, to abort the transaction) since
it can't be called a second time within the same transaction anyway
(because ->set_user must be called exactly once), so continuing with the
same transaction can't result in anything useful anyway.

In some places, error messages were slightly improved to contain a bit
more information.
2023-11-24 05:15:22 +01:00

107 lines
2.8 KiB
Perl

#!perl
HELP1 "<account>" => "[Pay with your account and] show balance";
HELP "list" => "List accounts and balances";
HELP "log" => "View transaction log";
HELP "shame" => "Display Hall of Shame (negative balances)";
sub command :Tab(list,ls,shame,log,USERS) ($self, $cart, $command, @) {
return $self->list if $command eq 'list';
return $self->list if $command eq 'ls';
return $self->shame if $command eq 'shame';
return "Username", \&log_for if $command eq 'log';
my $user = parse_user($command)
or return NEXT;
return $self->balance($user) if not $cart->size;
$cart->checkout($user);
return ACCEPT;
}
sub hook_checkout($class, $cart, $user, $transaction_id, @) {
if ($cart->changed) {
say "Done:";
$cart->display;
}
say "Transaction ID: $transaction_id";
}
sub list($self) {
require RevBank::TextEditor;
my $list = join "", sort {
lc($a) cmp lc($b)
} grep {
!/^[-+]/
} slurp("revbank.accounts");
RevBank::TextEditor::pager("RevBank account list", $list);
return ACCEPT;
}
sub shame($self) {
my $list = join "", sort {
(split " ", $a)[1] <=> (split " ", $b)[1]
} grep {
/ -/ && !/^[-+]/
} slurp("revbank.accounts");
$list =~ s/( -[\d.]+)/\e[31;1m$1\e[0m/g;
print $list;
return ACCEPT;
}
sub _grep($user) {
$user = lc $user;
my @lines;
open my $fh, "<", ".revbank.log" or die $!;
while (defined($_ = readline $fh)) {
length($_) > 28 or next;
substr($_, 20, 8) eq 'CHECKOUT' or next; # fast check
my ($dt, $c, $t_id, $u, $dir, $qty, $amount, undef, $desc) = split " ", $_, 9;
$c eq 'CHECKOUT' or next; # real check after expensive split
lc($u) eq $user or next;
$qty = 1 if $qty eq 'EUR'; # log files before commit 63f81e37 (2019-11-05)
push @lines, sprintf "%s %8s %s%-s", (
$dt =~ s/_/ /r,
$dir eq 'GAIN' ? "+ $amount" : $amount, # like R::A->string_flipped
$qty > 1 ? $qty . "x " : "",
$desc
);
}
return @lines;
}
sub log_for :Tab(USERS) ($self, $cart, $input, @) {
my $user = parse_user($input) or return REJECT, "Unknown user";
my @lines = _grep($user);
require RevBank::TextEditor;
RevBank::TextEditor::logpager("RevBank log for $user", join("", @lines, "(end)"));
return ACCEPT;
}
sub _recent($n, $u) {
$n += 0;
print "Last $n transactions for $u:\n";
print grep defined, +(_grep($u))[-$n .. -1];
}
sub balance($self, $u) {
_recent(10, $u);
call_hooks("user_info", $u);
my $balance = RevBank::Users::balance($u);
my $red = $balance->cents < 0 ? "31;" : "";
printf "Balance for $u is \e[%s1m%s\e[0m\n", $red, $balance->string("+");
say "NB: Products/amounts/commands FIRST, username LAST.";
return ABORT;
}