Eerste commit ever :)
This commit is contained in:
commit
4a535b7fee
2 changed files with 583 additions and 0 deletions
530
revbank
Executable file
530
revbank
Executable file
|
@ -0,0 +1,530 @@
|
|||
#!/usr/bin/perl -w
|
||||
use strict;
|
||||
no warnings 'exiting'; # We'll exit subs with 'next'
|
||||
use POSIX qw(strftime);
|
||||
use List::Util qw(sum);
|
||||
|
||||
$SIG{INT} = 'IGNORE';
|
||||
|
||||
# If you don't like the code, just rewrite it, but do keep all functionality
|
||||
# around, please.
|
||||
|
||||
# The user interface is based on supermarkets: first all products are scanned,
|
||||
# and then the user pays. In our case, "paying" is done simply by entering
|
||||
# an account name. No authentication...
|
||||
|
||||
# Although the text files are reloaded all the time, it's probably not a good
|
||||
# idea to edit them while someone else is using the bank script. They are,
|
||||
# however, human readable and editable.
|
||||
|
||||
# Likewise, things might go wrong if you run multiple instances of revbank...
|
||||
|
||||
sub logline {
|
||||
open my $fh, ">>.revbank.log";
|
||||
my $x = join "", @_;
|
||||
$x =~ s/\n/POSIX::strftime("\n%Y%m%d%H%M%S ", localtime)/ge;
|
||||
print {$fh} $x;
|
||||
close $fh;
|
||||
}
|
||||
|
||||
{
|
||||
package Tee;
|
||||
use base 'Tie::StdHandle';
|
||||
sub PRINT {
|
||||
shift;
|
||||
local $| = 1;
|
||||
print {*STDOUT} @_;
|
||||
main::logline(@_);
|
||||
}
|
||||
sub PRINTF {
|
||||
shift->PRINT(sprintf(shift, @_));
|
||||
}
|
||||
}
|
||||
|
||||
tie *TEE, 'Tee';
|
||||
select *TEE;
|
||||
|
||||
print "\e[0m\n\n\nWelcome to the RevBank Shell";
|
||||
|
||||
my $at = "ABORTING TRANSACTION.";
|
||||
|
||||
sub prompt {
|
||||
print "@_\e[1;4m";
|
||||
my $input = readline *STDIN;
|
||||
|
||||
logline(defined($input) ? $input : "\e[1;5mX\e[0m");
|
||||
|
||||
print "\e[0m";
|
||||
defined $input or return;
|
||||
chomp $input;
|
||||
|
||||
# Clean up backspaces... TODO: fix terminal :)
|
||||
$input =~ s/^\cH+//;
|
||||
$input =~ s/.\cH// while $input =~ /.\cH/;
|
||||
$input =~ s/^\cH+//;
|
||||
|
||||
if ($input =~ /^abort$/) {
|
||||
print "$at\n";
|
||||
next LINE; # Whoa, scary out-of-scope jump! But it works :)
|
||||
}
|
||||
return $input
|
||||
}
|
||||
|
||||
sub help {
|
||||
print <<END;
|
||||
|
||||
1. Enter products, amounts or commands
|
||||
2. Enter your name
|
||||
|
||||
You can press <Enter> after each element to get a follow-up prompt, or separate
|
||||
individual elements with whitespace.
|
||||
|
||||
Valid commands:
|
||||
<productID> Look up product from database
|
||||
<amount> Withdraw or enter price manually
|
||||
<user> Pay with your account
|
||||
deposit [<amount>] [Create and] deposit into an account
|
||||
give [<user>] [<amount>] Transfer money to user's account
|
||||
take [<users>] [<amount>] Take money from users (equal parts)
|
||||
list List accounts and balances
|
||||
undo [<id>] Undo a certain transaction
|
||||
help The stuff you're looking at right now :)
|
||||
examples Show some usage examples
|
||||
abort Abort the current transaction
|
||||
edit Edit product list
|
||||
END
|
||||
return ['noop'];
|
||||
}
|
||||
|
||||
sub examples {
|
||||
print <<END;
|
||||
Examples:
|
||||
4029764001807 jdoe John pays for one Club Mate.
|
||||
5 jdoe John withdraws EUR 5.00.
|
||||
.1 4029764001807 jdoe John pays 10 cents and one Club Mate.
|
||||
give 2.50 foo jdoe John gives Foo EUR 2.50.
|
||||
take 1 foo jdoe Jonh takes EUR 1.00 from Foo.
|
||||
take foo baz 15 jdoe John takes 7.50 from Foo and 7.50 from Baz.
|
||||
take foo baz jdoe 15 jdoe Split the BBQ bill that John paid: 3 * 5.
|
||||
deposit 5 jdoe John deposits EUR 5.00 into his account.
|
||||
|
||||
4029764001807 give foo 0.25 .5 take baz 2 deposit 10 help 4029764001807 jdoe
|
||||
John pays for a Club Mate, transfers 0.25 to Foo's account, donates
|
||||
50 cents, steals 2.00 from Baz, deposits 10 euro, displays the
|
||||
instructions, and pays for another Club Mate. John is elite, or drunk.
|
||||
|
||||
If you're unsure of the syntax, just type the command, press enter, and
|
||||
read the instructions.
|
||||
END
|
||||
return ['noop'];
|
||||
}
|
||||
|
||||
sub now {
|
||||
return strftime '%Y-%m-%d_%H:%M:%S', localtime;
|
||||
}
|
||||
|
||||
sub parse_product {
|
||||
my ($id) = @_;
|
||||
my @products;
|
||||
open my $fh, 'revbank.products' or die $!;
|
||||
/\S/ && !/^\s*#/ and push @products, [split " ", $_, 3] while readline $fh;
|
||||
close $fh;
|
||||
chomp @$_ for @products;
|
||||
for (@products) {
|
||||
return [ 'product', -$_->[1], $_->[2] ] if $_->[0] eq $id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub read_users {
|
||||
my @users;
|
||||
open my $fh, 'revbank.accounts' or die $!;
|
||||
/\S/ and push @users, [split " "] while readline $fh;
|
||||
close $fh;
|
||||
return @users;
|
||||
}
|
||||
|
||||
sub create_account {
|
||||
my ($username) = @_;
|
||||
open my $fh, '>>revbank.accounts' or die $!;
|
||||
my $now = now();
|
||||
print {$fh} "$username 0.00 $now\n" or die $!;
|
||||
close $fh or die $!;
|
||||
}
|
||||
|
||||
sub update_account {
|
||||
my ($username, $delta) = @_;
|
||||
open my $in, 'revbank.accounts' or die $!;
|
||||
open my $out, ">.revbank.$$" or die $!;
|
||||
my $old;
|
||||
my $new;
|
||||
while (defined (my $line = readline $in)) {
|
||||
my @a = split " ", $line;
|
||||
if (lc $a[0] eq lc $username) {
|
||||
$old = $a[1];
|
||||
$new = $old + $delta;
|
||||
printf {$out} "%-16s %+9.2f %s",
|
||||
$username, $new, now() or die $!;
|
||||
print {$out} "\n" or die $!;
|
||||
} else {
|
||||
print {$out} $line or die $!;
|
||||
}
|
||||
}
|
||||
close $out or die $!;
|
||||
close $in;
|
||||
rename ".revbank.$$", "revbank.accounts" or die $!;
|
||||
|
||||
my $sign = $delta >= 0 ? '+' : '-';
|
||||
printf "Balance for %s: %+.2f %s %.2f = %+.2f %s\n",
|
||||
$username, $old, $sign, abs($delta), $new,
|
||||
($new < -13.37 ? "\e[5;1m(!!)\e[0m" : "");
|
||||
}
|
||||
|
||||
sub parse_user {
|
||||
my ($id) = @_;
|
||||
my @users = read_users;
|
||||
for (@users) {
|
||||
return [ 'user', 0, $id ] if lc $_->[0] eq lc $id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub parse_amount {
|
||||
my ($amount) = @_;
|
||||
length $amount or return;
|
||||
if ($amount =~ /^(-)?[0-9]*(?:[,.][0-9]{1,2})?$/) {
|
||||
if ($1) {
|
||||
print "For our sanity, no negative amounts, please :). $at\n";
|
||||
next LINE;
|
||||
}
|
||||
$amount =~ s/,/./g;
|
||||
if ($amount > 999) {
|
||||
printf "Hm, EUR %.2f? Probably not. Unknown barcode? Enter price"
|
||||
. " instead.\n\e[1;5m'%s' IGNORED!!\e[0m\n", $amount, $amount;
|
||||
next ELEMENT;
|
||||
}
|
||||
return ['amount', 0 + $amount];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub parse_manual {
|
||||
my ($amount) = @_;
|
||||
my $p = parse_amount($amount) or return;
|
||||
return ['payment', -$p->[1], "Withdrawal or unlisted product"];
|
||||
}
|
||||
|
||||
sub parse_command {
|
||||
my ($command, $line) = @_;
|
||||
if ($command =~ /^(?:h|help|\?!?|wtf|omgwtfbbq)$/) {
|
||||
return help();
|
||||
} elsif ($command =~ /^(?:examples)$/) {
|
||||
return examples();
|
||||
} elsif ($command =~ /^(?:deposit)$/) {
|
||||
return deposit($line);
|
||||
} elsif ($command =~ /^(?:take|steal)$/) {
|
||||
return take($line);
|
||||
} elsif ($command =~ /^(?:give)$/) {
|
||||
return give($line);
|
||||
} elsif ($command =~ /^(?:undo)$/) {
|
||||
return undo($line);
|
||||
} elsif ($command =~ /^(?:ls|list)$/) {
|
||||
return list();
|
||||
} elsif ($command =~ /^(?:edit)$/) {
|
||||
return edit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub deposit {
|
||||
my ($line) = @_;
|
||||
my $p;
|
||||
if (@$line and $p = parse_amount($line->[0])) {
|
||||
shift @$line;
|
||||
return [ 'deposit', +$p->[1], "Deposit" ];
|
||||
} elsif (@$line) {
|
||||
print "$line->[0]: Invalid amount. $at\n";
|
||||
next LINE;
|
||||
}
|
||||
for (;;) {
|
||||
my $input = prompt "Amount to deposit into your account: ";
|
||||
if ($p = parse_amount($input)) {
|
||||
my $amount = $p->[1];
|
||||
return [ 'deposit', +$p->[1], "Deposit" ];
|
||||
}
|
||||
print "$input: Invalid amount. Type 'abort' to abort.\n";
|
||||
}
|
||||
}
|
||||
|
||||
sub give {
|
||||
my ($line) = @_;
|
||||
my $p;
|
||||
my $user;
|
||||
if (@$line and $p = parse_user($line->[0])) {
|
||||
shift @$line;
|
||||
$user = $p->[2];
|
||||
} elsif (@$line) {
|
||||
print "$line->[0]: Invalid user. $at\n";
|
||||
next LINE;
|
||||
}
|
||||
if (not $user) {
|
||||
for (;;) {
|
||||
my $input = prompt "Benificiary: ";
|
||||
if ($p = parse_user($input)) {
|
||||
$user = $p->[2];
|
||||
last;
|
||||
}
|
||||
print "$input: Invalid user. Type 'abort' to abort.\n";
|
||||
}
|
||||
}
|
||||
my $amount;
|
||||
if (@$line and $p = parse_amount($line->[0])) {
|
||||
shift @$line;
|
||||
$amount = $p->[1];
|
||||
} elsif (@$line) {
|
||||
print "$line->[0]: Invalid amount. $at\n";
|
||||
next LINE;
|
||||
}
|
||||
if (not defined $amount) {
|
||||
for (;;) {
|
||||
my $input = prompt "Amount to give to $user: ";
|
||||
if ($p = parse_amount($input)) {
|
||||
$amount = $p->[1];
|
||||
last;
|
||||
}
|
||||
print "$input: Invalid amount. Type 'abort' to abort.\n";
|
||||
}
|
||||
}
|
||||
return [ 'transfer', -$amount, "Given to $user", [
|
||||
[ $user, +$amount, "Received from %%%" ],
|
||||
] ];
|
||||
}
|
||||
|
||||
sub take {
|
||||
my ($line) = @_;
|
||||
my $p;
|
||||
my @users;
|
||||
my $amount;
|
||||
ELEMENT: while (@$line) {
|
||||
if ($p = parse_user($line->[0])) {
|
||||
shift @$line;
|
||||
push @users, $p->[2];
|
||||
next ELEMENT;
|
||||
}
|
||||
if ($p = parse_amount($line->[0])) {
|
||||
shift @$line;
|
||||
$amount = $p->[1];
|
||||
last ELEMENT;
|
||||
}
|
||||
print "$line->[0]: Invalid user or amount. $at\n";
|
||||
next LINE;
|
||||
}
|
||||
while (not @users or not defined $amount) {
|
||||
PROMPT: for (;;) {
|
||||
my $input = prompt
|
||||
"User to take from, or total amount to finish: ";
|
||||
if ($p = parse_user($input)) {
|
||||
push @users, $p->[2];
|
||||
next PROMPT;
|
||||
}
|
||||
if ($p = parse_amount($input)) {
|
||||
$amount = $p->[1];
|
||||
last PROMPT;
|
||||
}
|
||||
print "$input was not recognised as either an amount or a user.\n";
|
||||
}
|
||||
}
|
||||
my $each = sprintf "%.2f", $amount / @users;
|
||||
my $total = sprintf "%.2f", @users * $each;
|
||||
if ($total != $amount) {
|
||||
print "Adjusted total amount to $total because of rounding.\n";
|
||||
}
|
||||
my $x = @users > 1 ? 'each' : $users[0];
|
||||
my $reason;
|
||||
while (not $reason) {
|
||||
$reason = prompt
|
||||
"Why are you taking $each from $x? (e.g. 'bbq' or 'wok') ";
|
||||
}
|
||||
my $users = join "/", @users;
|
||||
return [ 'transfer', +$total, "Taken from $users ($reason)", [
|
||||
map [ $_, -$each, "Taken by %%% ($reason)" ], @users
|
||||
] ];
|
||||
}
|
||||
|
||||
sub undo {
|
||||
my ($line) = @_;
|
||||
my $tid;
|
||||
if (@$line and $line->[0] =~ /^[0-9]+$/) {
|
||||
$tid = shift @$line;
|
||||
}
|
||||
until ($tid) {
|
||||
$tid = prompt "Transaction ID to undo: ";
|
||||
}
|
||||
open my $in, ".revbank.undo" or die $!;
|
||||
open my $out, ">.revbank.undo.$$" or die $!;
|
||||
my $matches = 0;
|
||||
while (defined(my $line = readline $in)) {
|
||||
if ($line =~ /^$tid\s/) {
|
||||
$matches++;
|
||||
my (undef, $user, $delta) = split " ", $line;
|
||||
update_account($user, $delta);
|
||||
} else {
|
||||
print {$out} $line;
|
||||
}
|
||||
}
|
||||
close $in;
|
||||
close $out or die $!;
|
||||
if ($matches) {
|
||||
rename ".revbank.undo.$$", ".revbank.undo" or die $!;
|
||||
print "$matches account(s) affected by rollback.\n";
|
||||
} else {
|
||||
print "Transaction ID '$tid' not found in undo log.\n";
|
||||
}
|
||||
return ['noop'];
|
||||
}
|
||||
|
||||
sub list {
|
||||
system "sort -f revbank.accounts | grep -v ^# | more";
|
||||
return ['noop'];
|
||||
}
|
||||
|
||||
sub edit {
|
||||
system "vim revbank.products";
|
||||
return ['noop'];
|
||||
}
|
||||
|
||||
sub number_of {
|
||||
my ($type, @things) = @_;
|
||||
my $counter = 0;
|
||||
for (@things) {
|
||||
$counter++ if $_->[0] eq $type;
|
||||
}
|
||||
return $counter;
|
||||
}
|
||||
|
||||
sub transaction_sum {
|
||||
return sum map $_->[1], @_;
|
||||
}
|
||||
|
||||
sub todo2actions {
|
||||
my ($user, @todo) = @_;
|
||||
my %transactions;
|
||||
for my $t (@todo) {
|
||||
push @{ $transactions{$user} }, [ $t->[1], $t->[2] ];
|
||||
for (@{ $t->[3] }) {
|
||||
(my $desc = $_->[2]) =~ s/%%%/$user/g;
|
||||
push @{ $transactions{$_->[0]} }, [ $_->[1], $desc ];
|
||||
}
|
||||
}
|
||||
return %transactions;
|
||||
}
|
||||
|
||||
sub summary {
|
||||
my ($indent, $user, @todo) = @_;
|
||||
$user ||= '-you-';
|
||||
my %transactions = todo2actions($user, @todo);
|
||||
|
||||
my @users = sort keys %transactions;
|
||||
for my $u (@users) {
|
||||
my @tr = @{ $transactions{$u} };
|
||||
for my $tr (@tr) {
|
||||
printf(
|
||||
"$indent %-16s %4s EUR %5.2f # %s\n",
|
||||
$u,
|
||||
($tr->[0] > 0 ? 'GAIN' : $tr->[0] < 0 ? 'LOSE' : ''),
|
||||
abs($tr->[0]),
|
||||
$tr->[1]
|
||||
);
|
||||
}
|
||||
if (@tr > 1) {
|
||||
my $sum = sum(map $_->[0], @tr);
|
||||
printf(
|
||||
"$indent %-16s %4s EUR %5.2f TOTAL\n",
|
||||
$u,
|
||||
($sum > 0 ? 'GAIN' : $sum < 0 ? 'LOSE' : ''),
|
||||
abs($sum),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LINE: for (;;) {
|
||||
my @todo = ();
|
||||
print "\n";
|
||||
|
||||
my $user;
|
||||
while (not $user) {
|
||||
print "\n";
|
||||
@todo = grep { $_->[0] ne 'noop' } @todo;
|
||||
if (@todo) {
|
||||
print "Preview:\n";
|
||||
summary(" ", $user, @todo);
|
||||
print "\nEnter username to pay or 'abort' to abort.\n"
|
||||
}
|
||||
my $line = prompt "Product ID, amount or command: ";
|
||||
defined $line or exec $0;
|
||||
my @line = split " ", $line;
|
||||
ELEMENT: while (@line) {
|
||||
my $element = shift @line;
|
||||
my $p;
|
||||
|
||||
if ($p = parse_product($element) ) { push @todo, $p; next; }
|
||||
if ($p = parse_manual($element) ) { push @todo, $p; next; }
|
||||
if ($p = parse_command($element, \@line)) { push @todo, $p; next; }
|
||||
if ($p = parse_user($element)) {
|
||||
$user = $p->[2];
|
||||
last;
|
||||
}
|
||||
|
||||
my $deposit = grep { $_->[0] eq 'deposit' } @todo;
|
||||
if ($deposit) {
|
||||
print "Input '$element' not recognised.\n";
|
||||
for (;;) {
|
||||
my $yesno = prompt "Add new account for user '$element'? ";
|
||||
if ($yesno =~ /^(?:y|yes)$/) {
|
||||
create_account($element);
|
||||
$user = $element;
|
||||
next ELEMENT;
|
||||
} elsif ($yesno =~ /^(?:n|no)$/) {
|
||||
last;
|
||||
}
|
||||
print "Please enter y, yes, n, or no.\n";
|
||||
}
|
||||
}
|
||||
print "$element: No such product, user, or command. $at\n";
|
||||
next LINE;
|
||||
}
|
||||
if (@line) {
|
||||
print "Superfluous input (@line) after benificiary/remunerator "
|
||||
. "($user). $at\n";
|
||||
next LINE;
|
||||
}
|
||||
}
|
||||
@todo = grep { $_->[0] ne 'noop' } @todo;
|
||||
if (not @todo) {
|
||||
print "Products/amounts/commands FIRST, username LAST. $at\n";
|
||||
next LINE;
|
||||
}
|
||||
|
||||
my $tid = time() - 1300000000;
|
||||
print "/", "-" x 40, "\n";
|
||||
print "| Final (transaction ID = $tid):\n";
|
||||
summary("| ", $user, @todo);
|
||||
print "\\", "-" x 40, "\n";
|
||||
|
||||
open my $ufh, '>>.revbank.undo' or die $!;
|
||||
my %transactions = todo2actions($user, @todo);
|
||||
for (sort keys %transactions) {
|
||||
my $delta = sum map $_->[0], @{ $transactions{$_} };
|
||||
print {$ufh} join " ", $tid, $_, -$delta, now(), "\n" or die $!;
|
||||
my ($old, $new) = update_account($_, $delta);
|
||||
}
|
||||
close $ufh;
|
||||
|
||||
my $deposit = sum map $_->[1], grep { $_->[0] eq 'deposit' } @todo;
|
||||
if ($deposit) {
|
||||
printf "Don't forget to add EUR %.2f to the cash box!\n", $deposit;
|
||||
}
|
||||
|
||||
sleep 1; # Ensure new timestamp/id for new transaction
|
||||
}
|
53
revbank.products
Normal file
53
revbank.products
Normal file
|
@ -0,0 +1,53 @@
|
|||
649241869825 0.00 Free disgusting stuff
|
||||
|
||||
# Water
|
||||
5400151013112 0.50 Carbonated mineral water
|
||||
5400155056542 0.50 Mineral water
|
||||
|
||||
# Soda
|
||||
4017773042669 0.70 Power Up Energy Drink
|
||||
5449000131805 0.70 Coca-Cola Zero
|
||||
5449000006721 0.70 Fanta Cassis
|
||||
5449000009500 0.70 Coca-Cola Cherry
|
||||
5449000014535 0.70 Sprite
|
||||
5449000000996 0.70 Coca-Cola
|
||||
8711200175307 0.70 Lipton Ice Tea
|
||||
87156089 0.70 Sisi
|
||||
5449000046390 0.70 Schweppes Indian Tonic
|
||||
5449000067715 0.70 Dr Pepper
|
||||
8717662960000 0.70 Spam
|
||||
|
||||
# US Soda
|
||||
07811403 1.00 Canada Dry
|
||||
061500003366 1.00 Stars & Stripes
|
||||
07818707 1.00 A&W Root Beer
|
||||
01208500 1.00 MTN DEW
|
||||
01206201 1.00 Mountain Dew Code Red
|
||||
|
||||
# Other drinks
|
||||
4009418308902 0.70 Orange Juice
|
||||
5410228142027 0.75 Jupiler
|
||||
4029764001807 1.50 Club-Mate
|
||||
4029764001883 1.50 Club-Mate Cola
|
||||
4029764001814 1.50 Club-Mate IceT
|
||||
|
||||
# Chips
|
||||
05414359710322 0.50 Chips
|
||||
05414359710315 0.50 Chips
|
||||
zakjechips 0.50 Chips
|
||||
|
||||
# Candy
|
||||
5000159407236 0.70 Mars
|
||||
7613032630409 0.70 Lion White
|
||||
7613032634605 0.70 Lion King Size (temp. discount)
|
||||
5000159383943 0.70 Mars Planets
|
||||
5000159304245 0.70 M&M's Crispy
|
||||
40114606 0.70 KitKat
|
||||
40111445 0.70 M&M's Peanut
|
||||
7613032850029 0.70 KitKat Chunky
|
||||
80346401 0.70 Crunch
|
||||
40111490 0.70 M&M's Choco
|
||||
5000159407397 0.70 Snickers
|
||||
8710412465008 0.70 Sultana Bosvruchten
|
||||
87304473 0.70 Sultana Appel
|
||||
|
Loading…
Add table
Reference in a new issue