v9.0.0: rename 'user' to 'account' where appropriate

This commit is contained in:
Juerd Waalboer 2025-04-10 23:03:55 +02:00
parent 996159a2ad
commit 4c90277fa9
33 changed files with 249 additions and 172 deletions

View file

@ -10,7 +10,7 @@ Since version 2, RevBank is loosely modeled after C<qpsmtpd>, which is an SMTP s
RevBank is interactive and stateful. Global state is provided in the form of a "shopping cart", a L<RevBank::Cart> object, which represents the ongoing, unfinished, transaction. The terms "cart", "unfinished transaction", and "current transaction" generally all refer to the same thing in the context of RevBank.
In addition, RevBank provides the concept of accounts through L<RevBank::Users>. There are user accounts and internal accounts; internal accounts are used as contra accounts for I<double-entry bookkeeping>, and are hidden from the user interface. Accounts only have a name, a balance, and some timestamps; things like transaction histories are provided by plugins.
In addition, RevBank provides the concept of accounts through L<RevBank::Accounts>. There are user accounts and hidden accounts; hidden accounts are used as contra accounts for I<double-entry bookkeeping>, and are hidden from the user interface. Accounts only have a name, a balance, and some timestamps; things like transaction histories are provided by plugins.
Notably, the RevBank core does B<not> have any notion of "products". Support for buying products through RevBank is provided by plugins, like the included C<products> and C<market> plugins. It is easy to add another source of products by writing another plugin. A plugin contains arbitrary code and can do anything, including querying external resources.

View file

@ -1,4 +1,4 @@
package RevBank::Users;
package RevBank::Accounts;
use v5.32;
use warnings;
@ -12,7 +12,7 @@ use List::Util ();
my $filename = "revbank.accounts";
sub _read() {
my @users;
my @accounts;
for my $line (slurp $filename) {
$line =~ /\S/ or next;
# Not using RevBank::Prompt::split_input to keep parsing by external
@ -25,23 +25,23 @@ sub _read() {
@split = split " ", $line, 2;
}
push @users, \@split;
push @accounts, \@split;
}
my %users;
for (@users) {
my %accounts;
for (@accounts) {
my $name = lc $_->[0];
exists $users{$name} and die "$filename: duplicate entry '$name'\n";
$users{$name} = $_;
exists $accounts{$name} and die "$filename: duplicate entry '$name'\n";
$accounts{$name} = $_;
if ($name =~ s/^\*//) {
# user-accessible special account: support without * prefix
exists $users{$name} and die "$filename: duplicate entry '$name'\n";
$users{$name} = $_;
exists $accounts{$name} and die "$filename: duplicate entry '$name'\n";
$accounts{$name} = $_;
}
}
return \%users;
return \%accounts;
}
sub names() {
@ -50,25 +50,26 @@ sub names() {
return List::Util::uniqstr map $_->[0], values %{ _read() };
}
sub balance($username) {
return RevBank::Amount->parse_string( _read()->{ lc $username }->[1] );
sub balance($account) {
return RevBank::Amount->parse_string( _read()->{ lc $account }->[1] );
}
sub since($username) {
return _read()->{ lc $username }->[3];
sub since($account) {
return _read()->{ lc $account }->[3];
}
sub create($username) {
die "Account already exists" if exists _read()->{ lc $username };
sub create($account) {
die "Account already exists" if exists _read()->{ lc $account };
my $now = now();
append $filename, "$username 0.00 $now\n";
RevBank::Plugins::call_hooks("user_created", $username);
return $username;
append $filename, "$account 0.00 $now\n";
RevBank::Plugins::call_hooks("user_created", $account); # until 2027-05-01
RevBank::Plugins::call_hooks("account_created", $account);
return $account;
}
sub update($username, $delta, $transaction_id) {
my $account = assert_user($username) or die "No such user ($username)";
sub update($account, $delta, $transaction_id) {
$account = assert_account($account);
my $old = RevBank::Amount->new(0);
my $new = RevBank::Amount->new(0);
@ -99,24 +100,28 @@ sub update($username, $delta, $transaction_id) {
};
RevBank::Plugins::call_hooks(
# Backwards compatibility until 2027-05-01
"user_balance", $account, $old, $delta, $new, $transaction_id
);
RevBank::Plugins::call_hooks(
"account_balance", $account, $old, $delta, $new, $transaction_id
);
}
sub is_hidden($username) {
return $username =~ /^[-+]/;
sub is_hidden($account) {
return $account =~ /^[-+]/;
}
sub is_special($username) {
return $username =~ /^[-+*]/;
sub is_special($account) {
return $account =~ /^[-+*]/;
}
sub parse_user($username, $allow_invalid = 0) {
return undef if is_hidden($username);
my $users = _read();
my $accounts = _read();
my $user = $users->{ lc $username } or return undef;
my $user = $accounts->{ lc $username } or return undef;
if ($user->[1] =~ /^!(.*)/) {
warn "$username: Invalid account ($1).\n";
@ -128,21 +133,32 @@ sub parse_user($username, $allow_invalid = 0) {
return $user->[0];
}
sub assert_user($username) {
my $users = _read();
sub assert_account($account) {
my $accounts = _read();
my $user = $users->{ lc $username };
my $account_info = $accounts->{ lc $account };
if ($user) {
Carp::croak("Account $username can't be used") if not defined balance $username;
return $user->[0];
if ($account) {
Carp::croak("Account $account can't be used") if not defined balance $account;
return $account_info->[0];
}
return create $username if is_hidden $username;
return create $account if is_hidden $account;
Carp::croak("No such user ($username)")
Carp::croak("No such user ($account)");
}
# Backwards compatibility until 2027-05-01
*RevBank::Users::names = \&RevBank::Accounts::names;
*RevBank::Users::balance = \&RevBank::Accounts::balance;
*RevBank::Users::since = \&RevBank::Accounts::since;
*RevBank::Users::create = \&RevBank::Accounts::create;
*RevBank::Users::update = \&RevBank::Accounts::update;
*RevBank::Users::is_hidden = \&RevBank::Accounts::is_hidden;
*RevBank::Users::is_special = \&RevBank::Accounts::is_special;
*RevBank::Users::parse_user = \&RevBank::Accounts::parse_user;
*RevBank::Users::assert_user = \&RevBank::Accounts::assert_account;
1;

View file

@ -1,10 +1,10 @@
=head1 NAME
RevBank::Users - Banking and bookkeeping accounts
RevBank::Accounts - Banking and bookkeeping accounts
=head1 DESCRIPTION
This package handles all accounts in RevBank. Accounts are called "users" because originally, RevBank only had user accounts. Today, RevBank does doubly-entry bookkeeping and has multiple account types to accommodate that.
This package handles all accounts in RevBank. RevBank does doubly-entry bookkeeping and has multiple account types to accommodate that.
This package is where manipulation of C<revbank.accounts> happens.
@ -16,6 +16,10 @@ This package is where manipulation of C<revbank.accounts> happens.
User accounts are typically made with the C<adduser> command, and almost all interactions with RevBank will involve only user accounts, from the perspective of the user.
The name of a user account is called a I<username> within RevBank.
Any account that does not begin with one of the characters C<->, C<+>, or C<*>, is a user account.
=item * Hidden accounts
The name of a hidden account begins with a C<-> or C<+> sign. These accounts are created automatically by plugins to provide the I<double> part in I<doubly-entry bookkeeping>.
@ -100,9 +104,9 @@ Only the first two columns are mandatory. This makes migrating to RevBank very s
=head2 Functions
Usernames are case preserving, but case insensitive. Account name arguments to functions are case insensitive, but return values use the canonical capitalization.
Account names are case preserving, but case insensitive. Account name arguments to functions are case insensitive, but return values use the canonical capitalization.
Anything that outputs a username should always run it through C<parse_user> or C<assert_user>.
Anything that outputs a username should always run it through C<parse_user> or C<assert_account>.
=head3 names
@ -120,13 +124,13 @@ Returns the last used datetime of the account.
Creates an account with that name and a balance of zero. The name must not already exist.
After updating the file, calls the C<user_created> hook with the account name.
After updating the file, calls the C<account_created> hook with the account name.
=head3 update($name, $delta, $transaction_id)
Given the relative change (C<$delta>), updates the user balance for an account.
After updating the file, calls the C<user_balance> hook with the account name, the old balance, the given delta, the new balance, and the transaction_id.
After updating the file, calls the C<account_balance> hook with the account name, the old balance, the given delta, the new balance, and the transaction_id.
This function should not be used directly; instead, create a transaction via C<RevBank::Cart> and use C<checkout> to ensure a balanced booking for proper double-entry bookkeeping.
@ -140,17 +144,19 @@ Returns true if the account is hidden (begins with C<+> or C<->), or user-access
=head3 parse_user($username)
Returns the canonical account name if the user account exists, or undef if it does not exist.
Returns the canonical account name if the account exists and is not a hidden account, or undef otherwise.
=head3 assert_user($name)
=head3 assert_account($name)
For a hidden account, returns the canonical account name, creating the account if it did not already exist.
For a non-hidden account, works like parse_user.
For a non-hidden account, returns the canonical account name if the account exists, or throws an exception if it does not exist.
=head1 CAVEATS
=head1 HISTORY
The identifiers can be confusing and most instances of C<user> should probably be renamed to C<account>.
Originally, RevBank had only user accounts, and the package was called C<RevBank::Users>. When hidden (internal) accouns were added, account names were still always in variables called C<$user> or C<$username> even if they were hidden accounts and thus not accessible to users. In current RevBank, the term I<account> is used as the generic thing, or I<user> only in places where only user accounts (non-hidden accounts) are supported.
This change took place in 2025, and some backwards compatibility will be kept until at least 2027-05-01. See UPGRADING.md for more information.
=head1 AUTHOR

View file

@ -7,7 +7,7 @@ use experimental 'signatures'; # stable since v5.36
use Carp ();
use List::Util ();
use RevBank::Global;
use RevBank::Users;
use RevBank::Accounts;
use RevBank::FileIO;
use RevBank::Cart::Entry;
@ -84,11 +84,11 @@ sub prohibit_checkout($self, $bool, $reason) {
}
}
sub deltas($self, $user) {
my %deltas = ($user => RevBank::Amount->new(0));
sub deltas($self, $account) {
my %deltas = ($account => RevBank::Amount->new(0));
for my $entry (@{ $self->{entries} }) {
$deltas{$_->{user}} += $_->{amount} * $entry->quantity
$deltas{$_->{account}} += $_->{amount} * $entry->quantity
for $entry, $entry->contras;
}
@ -96,7 +96,7 @@ sub deltas($self, $user) {
}
sub checkout($self, $user) {
sub checkout($self, $account) {
if ($self->{prohibited}) {
die RevBank::Cart::CheckoutProhibited->new(
"Cannot complete transaction: $self->{prohibited}"
@ -108,13 +108,13 @@ sub checkout($self, $user) {
die "Refusing to finalize deficient transaction";
}
$user = RevBank::Users::assert_user($user);
$account = RevBank::Accounts::assert_account($account);
my $entries = $self->{entries};
for my $entry (@$entries) {
$entry->sanity_check;
$entry->user($user);
$entry->account($account);
}
RevBank::FileIO::with_lock {
@ -133,28 +133,28 @@ sub checkout($self, $user) {
$transaction_id = time() - 1300000000;
}
RevBank::Plugins::call_hooks("checkout_prepare", $self, $user, $transaction_id)
RevBank::Plugins::call_hooks("checkout_prepare", $self, $account, $transaction_id)
or die "Refusing to finalize after failed checkout_prepare";
for my $entry (@$entries) {
$entry->sanity_check;
$entry->user($user) if not $entry->user;
$entry->account($account) if not $entry->account;
}
RevBank::FileIO::spurt($fn, ++(my $next_id = $transaction_id)) unless $legacy_id;
RevBank::Plugins::call_hooks("checkout", $self, $user, $transaction_id);
RevBank::Plugins::call_hooks("checkout", $self, $account, $transaction_id);
my $deltas = $self->deltas($user);
my $deltas = $self->deltas($account);
for my $account (reverse sort keys %$deltas) {
# The reverse sort is a lazy way to make the "-" accounts come last,
# which looks nicer with the "cash" plugin.
RevBank::Users::update($account, $deltas->{$account}, $transaction_id)
RevBank::Accounts::update($account, $deltas->{$account}, $transaction_id)
if $deltas->{$account} != 0;
}
RevBank::Plugins::call_hooks("checkout_done", $self, $user, $transaction_id);
RevBank::Plugins::call_hooks("checkout_done", $self, $account, $transaction_id);
sleep 1; # look busy

View file

@ -5,7 +5,7 @@ use warnings;
use experimental 'signatures'; # stable since v5.36
use Carp qw(carp croak);
use RevBank::Users;
use RevBank::Accounts;
use List::Util ();
use Scalar::Util ();
@ -24,7 +24,7 @@ sub new($class, $amount, $description, $attributes = {}) {
amount => $amount, # negative = pay, positive = add money
description => $description,
attributes => { %$attributes },
user => undef,
account => undef,
contras => [],
caller => List::Util::first(sub { !/^RevBank::Cart/ }, map { (caller $_)[3] } 1..10)
|| (caller 1)[3],
@ -34,20 +34,21 @@ sub new($class, $amount, $description, $attributes = {}) {
return bless $self, $class;
}
sub add_contra($self, $user, $amount, $description, $display = undef) {
sub add_contra($self, $account, $amount, $description, $display = undef) {
# $display should be given for either ALL or NONE of the contras,
# with the exception of contras with $amount == 0.00;
$amount = RevBank::Amount->parse_string($amount) if not ref $amount;
$user = RevBank::Users::assert_user($user);
$account = RevBank::Accounts::assert_account($account);
$description =~ s/\$you/$self->{user}/g if defined $self->{user};
$description =~ s/\$you/$self->{account}/g if defined $self->{account};
push @{ $self->{contras} }, {
user => $user,
amount => $amount, # should usually have opposite sign (+/-)
description => $description, # contra user's perspective
display => $display, # interactive user's perspective
account => $account,
user => $account, # backwards compatibility until 2027-05-01
amount => $amount, # should usually have opposite sign (+/-)
description => $description, # contra account's perspective
display => $display, # interactive user's perspective
highlight => 1,
};
@ -133,9 +134,9 @@ sub as_printable($self) {
for my $c (@{ $self->{contras} }) {
my $description;
my $amount = $self->{amount};
my $hidden = RevBank::Users::is_hidden($c->{user});
my $hidden = RevBank::Accounts::is_hidden($c->{account});
my $fromto = $c->{amount}->cents < 0 ? "<-" : "->";
$fromto .= " $c->{user}";
$fromto .= " $c->{account}";
if ($c->{display}) {
$description =
@ -165,7 +166,7 @@ sub as_printable($self) {
}
sub as_loggable($self) {
croak "Loggable called before set_user" if not defined $self->{user};
croak "Loggable called before set_account" if not defined $self->{account};
my $quantity = $self->{quantity};
@ -180,7 +181,7 @@ sub as_loggable($self) {
push @s, sprintf(
"%-12s %4s %3d %6s # %s",
$_->{user},
$_->{account},
($total->cents > 0 ? 'GAIN' : $total->cents < 0 ? 'LOSE' : '===='),
$quantity,
$total->abs,
@ -191,17 +192,20 @@ sub as_loggable($self) {
return @s;
}
sub user($self, $new = undef) {
sub account($self, $new = undef) {
if (defined $new) {
croak "User can only be set once" if defined $self->{user};
croak "User can only be set once" if defined $self->{account};
$self->{user} = $new;
$self->{account} = $new;
$self->{user} = $new; # backwards compatibility until 2027-05-01
$_->{description} =~ s/\$you/$new/g for $self, @{ $self->{contras} };
}
return $self->{user};
return $self->{account};
}
*user = \&account; # backwards compatibility until 2027-05-01
sub sanity_check($self) {
my @contras = $self->contras;

View file

@ -16,7 +16,7 @@ use RevBank::FileIO;
sub import {
require RevBank::Plugins;
require RevBank::Users;
require RevBank::Accounts;
no strict 'refs';
my $caller = caller;
*{"$caller\::ACCEPT"} = sub () { \1 };
@ -30,7 +30,7 @@ sub import {
*{"$caller\::rewrite"} = \&RevBank::FileIO::rewrite;
*{"$caller\::append"} = \&RevBank::FileIO::append;
*{"$caller\::with_lock"} = \&RevBank::FileIO::with_lock;
*{"$caller\::parse_user"} = \&RevBank::Users::parse_user;
*{"$caller\::parse_user"} = \&RevBank::Accounts::parse_user;
*{"$caller\::parse_amount"} = sub ($amount) {
defined $amount or return undef;
length $amount or return undef;

View file

@ -32,10 +32,10 @@ Commas are changed to periods so C<3,50> and C<3.50> both result in C<3.5>.
=head2 parse_user($username)
See C<parse_user> in L<RevBank::Users>.
Returns the canonical username, or undef if the account does not exist.
See C<parse_user> in L<RevBank::Accounts> for the gory details.
=head1 AUTHOR
Juerd Waalboer <#####@juerd.nl>

View file

@ -12,7 +12,7 @@ use base 'RevBank::Plugin';
BEGIN {
RevBank::Plugins::register("RevBank::Messages");
*hidden = \&RevBank::Users::is_hidden;
*hidden = \&RevBank::Accounts::is_hidden;
}
@ -41,7 +41,7 @@ sub hook_cart_changed($class, $cart, @) {
}
}
sub hook_checkout($class, $cart, $user, $transaction_id, @) {
sub hook_checkout($class, $cart, $account, $transaction_id, @) {
if ($cart->changed) {
say "Done:";
$cart->display;
@ -66,8 +66,8 @@ sub hook_reject($class, $plugin, $reason, $abort, @) {
say $abort ? $reason : "$reason Enter 'abort' to abort.";
}
sub hook_user_balance($class, $username, $old, $delta, $new, @) {
return if hidden $username and not $ENV{REVBANK_DEBUG};
sub hook_account_balance($class, $account, $old, $delta, $new, @) {
return if hidden $account and not $ENV{REVBANK_DEBUG};
my $sign = $delta->cents >= 0 ? '+' : '-';
my $rood = $new->cents < 0 ? '31;' : '';
@ -75,13 +75,13 @@ sub hook_user_balance($class, $username, $old, $delta, $new, @) {
my $warn = $new->cents < -2300 ? " \e[5;1m(!!)\e[0m" : "";
$_ = $_->string("+") for $old, $new;
printf "New balance for $username: $old $sign $abs = \e[${rood}1m$new\e[0m$warn\n",
printf "New balance for $account: $old $sign $abs = \e[${rood}1m$new\e[0m$warn\n",
}
sub hook_user_created($class, $username, @) {
return if hidden $username and not $ENV{REVBANK_DEBUG};
sub hook_account_created($class, $account, @) {
return if hidden $account and not $ENV{REVBANK_DEBUG};
say "New account '$username' created.";
say "New account '$account' created.";
}
1;

View file

@ -33,8 +33,8 @@ sub Tab($self, $method) {
}
if (delete $completions{USERS}) {
for my $name (RevBank::Users::names()) {
next if RevBank::Users::is_hidden($name);
for my $name (RevBank::Accounts::names()) {
next if RevBank::Accounts::is_hidden($name);
$completions{ $name }++;
$completions{ $1 }++ if $name =~ /^\*(.*)/;

View file

@ -145,10 +145,10 @@ Called when user input was given. C<$split_input> is a boolean that is true
if the input will be split on whitespace, rather than treated as a whole.
The input MAY be altered by the plugin.
=item hook_add($class, $cart, $user, $item, @)
=item hook_add($class, $cart, $account, $item, @)
Called when something is added to the cart. Of course, like in C<< $cart->add
>>, C<$user> will be undef if the product is added for the current user.
>>, C<$account> will be undef if the product is added for the current user.
C<$item> is a reference to a hash with the keys C<amount>, C<description> and
the metadata given in the C<add> call. Changing the values changes the actual
@ -156,15 +156,15 @@ item going into the cart!
Be careful to avoid infinite loops if you add new stuff.
=item hook_checkout_prepare($class, $cart, $user, $transaction_id, @)
=item hook_checkout_prepare($class, $cart, $account, $transaction_id, @)
Called when the transaction is about to be processed. In this phase, the cart and its entries can still be manipulated. If the hook throws an exception, the transaction is aborted.
=item hook_checkout($class, $cart, $user, $transaction_id, @)
=item hook_checkout($class, $cart, $account, $transaction_id, @)
Called when the transaction is finalized, before accounts are updated. The cart and cart entries must not be changed.
=item hook_checkout_done($class, $cart, $user, $transaction_id, @)
=item hook_checkout_done($class, $cart, $account, $transaction_id, @)
Called when the transaction is finalized, after accounts were updated.
@ -181,13 +181,13 @@ Called when input was not recognised by any of the plugins.
Called when a plugin fails.
=item hook_user_created($class, $username, @)
=item hook_account_created($class, $account, @)
Called when a new user account was created.
Called when a new account was created.
=item hook_user_balance($class, $username, $old, $delta, $new, $transaction_id, @)
=item hook_account_balance($class, $account, $old, $delta, $new, $transaction_id, @)
Called when a user account is updated.
Called when an account is updated.
=item hook_products_changed($class, $changes, $mtime, @)