From 4a535b7fee04a364e937b78681e8a28f7a2fdb2f Mon Sep 17 00:00:00 2001 From: bar Date: Mon, 25 Apr 2011 23:00:36 +0200 Subject: [PATCH] Eerste commit ever :) --- revbank | 530 +++++++++++++++++++++++++++++++++++++++++++++++ revbank.products | 53 +++++ 2 files changed, 583 insertions(+) create mode 100755 revbank create mode 100644 revbank.products diff --git a/revbank b/revbank new file mode 100755 index 0000000..c0a4ea1 --- /dev/null +++ b/revbank @@ -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 < after each element to get a follow-up prompt, or separate +individual elements with whitespace. + +Valid commands: + Look up product from database + Withdraw or enter price manually + Pay with your account + deposit [] [Create and] deposit into an account + give [] [] Transfer money to user's account + take [] [] Take money from users (equal parts) + list List accounts and balances + undo [] 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 <[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 +} diff --git a/revbank.products b/revbank.products new file mode 100644 index 0000000..f024165 --- /dev/null +++ b/revbank.products @@ -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 +