| Finance-Bank-Postbank_de documentation | Contained in the Finance-Bank-Postbank_de distribution. |
Finance::Bank::Postbank_de::Account - Postbank bank account class
use strict;
require Crypt::SSLeay; # It's a prerequisite
use Finance::Bank::Postbank_de::Account;
my $statement = Finance::Bank::Postbank_de::Account->parse_statement(
number => '9999999999',
password => '11111',
);
# Retrieve account data :
print "Balance : ",$statement->balance->[1]," EUR\n";
# Output CSV for the transactions
for my $row ($statement->transactions) {
print join( ";", map { $row->{$_} } (qw( tradedate valuedate type comment receiver sender amount ))),"\n";
};
This module provides a rudimentary interface to the Postbank online banking system at https://banking.postbank.de/. You will need either Crypt::SSLeay or IO::Socket::SSL installed for HTTPS support to work with LWP.
The interface was cooked up by me without taking a look at the other Finance::Bank modules. If you have any proposals for a change, they are welcome !
This is code for online banking, and that means your money, and that means BE CAREFUL. You are encouraged, nay, expected, to audit the source of this module yourself to reassure yourself that I am not doing anything untoward with your banking data. This software is useful to me, but is provided under NO GUARANTEE, explicit or implied.
Dieser Code beschaeftigt sich mit Online Banking, das heisst, hier geht es um Dein Geld und das bedeutet SEI VORSICHTIG ! Ich gehe davon aus, dass Du den Quellcode persoenlich anschaust, um Dich zu vergewissern, dass ich nichts unrechtes mit Deinen Bankdaten anfange. Diese Software finde ich persoenlich nuetzlich, aber ich stelle sie OHNE JEDE GARANTIE zur Verfuegung, weder eine ausdrueckliche noch eine implizierte Garantie.
Creates a new object. It takes three named parameters :
This is the number of the account. If you don't know it (for example, you are reading in an account statement from disk), leave it undef.
Parses an account statement and returns it as a hash reference. The account statement
can be passed in via two named parameters. If no parameter is given, the current statement
is fetched via the website through a call to get_account_statement (is this so?).
Parameters :
Parses the file $filename instead of downloading data from the web.
Parses the content of $string instead of downloading data from the web.
Returns the IBAN for the account as a string. Later, a move to Business::IBAN is planned. The IBAN is a unique identifier for every account, that identifies the country, bank and account with that bank.
Delivers you all transactions within a statement. The transactions may be filtered by date by specifying the parameters 'since', 'upto' or 'on'. The values are, as always, 8-digit strings denoting YYYYMMDD dates.
Parameters :
Removes all transactions that happened on or before $date. $date must
be in the format YYYYMMDD. If the line is missing, since => '00000000'
is assumed.
Removes all transactions that happened after $date. $date must
be in the format YYYYMMDD. If the line is missing, upto => '99999999'
is assumed.
Removes all transactions that happened on a date that is not eq to $date. $date must
be in the format YYYYMMDD. $date may also be the special string 'today', which will
be converted to a YYYYMMDD string corresponding to todays date.
value_dates is a convenience method that returns all value dates on the account statement.
trade_dates is a convenience method that returns all trade dates on the account statement.
#!/usr/bin/perl -w
use strict;
use Finance::Bank::Postbank_de::Account;
use Tie::File;
use List::Sliding::Changes qw(find_new_elements);
use FindBin;
use MIME::Lite;
my $filename = "$FindBin::Bin/statement.txt";
tie my @statement, 'Tie::File', $filename
or die "Couldn't tie to '$filename' : $!";
my @transactions;
# See what has happened since we last polled
my $retrieved_statement = Finance::Bank::Postbank_de::Account->parse_statement(
number => '9999999999',
password => '11111',
);
# Output CSV for the transactions
for my $row (reverse @{$retrieved_statement->transactions()}) {
push @transactions, join( ";", map { $row->{$_} } (qw( tradedate valuedate type comment receiver sender amount )));
};
# Find out what we did not already communicate
my (@new) = find_new_elements(\@statement,\@transactions);
if (@new) {
my ($body) = "<html><body><table>";
my ($date,$balance) = @{$retrieved_statement->balance};
$body .= "<b>Balance ($date) :</b> $balance<br>";
$body .= "<tr><th>";
$body .= join( "</th><th>", qw( tradedate valuedate type comment receiver sender amount )). "</th></tr>";
for my $line (@{[@new]}) {
$line =~ s!;!</td><td>!g;
$body .= "<tr><td>$line</td></tr>\n";
};
$body .= "</body></html>";
MIME::Lite->new(
From =>'update.pl',
To =>'you',
Subject =>"Account update $date",
Type =>'text/html',
Encoding =>'base64',
Data => $body,
)->send;
};
# And update our log with what we have seen
push @statement, @new;
Max Maischein, <corion@cpan.org>
perl, Finance::Bank::Postbank_de.
| Finance-Bank-Postbank_de documentation | Contained in the Finance-Bank-Postbank_de distribution. |
package Finance::Bank::Postbank_de::Account; use strict; use warnings; use Carp qw(croak); use POSIX qw(strftime); use Finance::Bank::Postbank_de; use base 'Class::Accessor'; use vars qw[ $VERSION %tags %totals %columns %safety_check ]; $VERSION = '0.25'; BEGIN { Finance::Bank::Postbank_de::Account->mk_accessors(qw( number balance balance_unavailable balance_prev transactions_future iban blz account_type name)); }; sub new { my $self = $_[0]->SUPER::new(); my ($class,%args) = @_; my $num = delete $args{number} || delete $args{kontonummer}; croak "'kontonummer' is '$args{kontonummer}' and 'number' is '$num'" if $args{kontonummer} and $args{kontonummer} ne $num; $self->number($num) if (defined $num); $self->name($args{name}) if (exists $args{name}); $self; }; *kontonummer = *number; %safety_check = ( name => 1, kontonummer => 1, ); %tags = ( Girokonto => [qw(Name BLZ Kontonummer IBAN)], Tagesgeldkonto => [qw(Name BLZ Kontonummer)], Sparcard => [qw(Name BLZ Kontonummer )], Sparkonto => [qw(Name BLZ Kontonummer )], Kreditkarte => [qw(Name BLZ Kontonummer IBAN)], ); %totals = ( Girokonto => [ [qr'^Aktueller Kontostand' => 'balance'], [qr'^Summe vorgemerkter Ums.tze' => 'transactions_future'], [qr'^Davon noch nicht verf.gbar' => 'balance_unavailable'], ], Sparcard => [[qr'Aktueller Kontostand' => 'balance'],], Sparkonto => [[qr'Aktueller Kontostand' => 'balance'],], Tagesgeldkonto => [[qr'Aktueller Kontostand' => 'balance'],], ); %columns = ( qr'Datum' => 'tradedate', qr'Wertstellung' => 'valuedate', qr'Art' => 'type', qr'Buchungshinweis' => 'comment', qr'Verwendungszweck' => 'comment', qr'Auftraggeber' => 'sender', qr'Empf.nger' => 'receiver', qr'Betrag Euro' => 'amount', qr'Saldo Euro' => 'running_total', ); sub parse_date { my ($self,$date) = @_; $date =~ /^(\d{2})\.(\d{2})\.(\d{4})$/ or die "Unknown date format '$date'. A date must be in the format 'DD.MM.YYYY'\n"; $3.$2.$1; }; sub parse_amount { my ($self,$amount) = @_; die "String '$amount' does not look like a number" unless $amount =~ /^-?[0-9]{1,3}(?:\.\d{3})*,\d{2}$/; $amount =~ tr/.//d; $amount =~ s/,/./; $amount; }; sub slurp_file { my ($self,$filename) = @_; local $/ = undef; local *F; open F, "< $filename" or croak "Couldn't read from file '$filename' : $!"; <F>; }; sub parse_statement { my ($self,%args) = @_; # If $self is just a string, we want to make a new class out of us $self = $self->new unless ref $self; my $filename = $args{file}; my $raw_statement = $args{content}; if ($filename) { $raw_statement = $self->slurp_file($filename); } elsif (! defined $raw_statement) { croak "Need an account number if I have to retrieve the statement online" unless $args{number}; croak "Need a password if I have to retrieve the statement online" unless exists $args{password}; my $login = $args{login} || $args{number}; return Finance::Bank::Postbank_de->new( login => $login, password => $args{password} )->get_account_statement; }; croak "Don't know what to do with empty content" unless $raw_statement; my @lines = split /\r?\n/, $raw_statement; croak "No valid account statement: '$lines[0]'" unless $lines[0] =~ /^Kontoumsätze Postbank (.*)$/; shift @lines; my $account_type = $1; croak "Unknown account type '$account_type'" unless exists $tags{$account_type}; $self->account_type($account_type); $lines[0] =~ m!^\s*$! or croak "Expected an empty line as the second line, got '$lines[0]'"; shift @lines; # Name: PETRA PFIFFIG for my $tag (@{ $tags{ $self->account_type }||[] }) { $lines[0] =~ /^\Q$tag\E: (.*)$/ or croak "Field '$tag' not found in account statement ($lines[0])"; my $method = lc($tag); my $value = $1; # special check for special fields: croak "Wrong/mixed account $method: Got '$value', expected '" . $self->$method . "'" if (exists $safety_check{$method} and defined $self->$method and $self->$method ne $value); $self->$method($value); shift @lines; }; if ($lines[0] !~ m!^\s*$!) { local $" = "|"; croak "Expected an empty line after the information, got '$lines[0]'"; }; shift @lines; while ($lines[0] !~ /^\s*$/) { my $line = shift @lines; my ($method,$balance); for my $total (@{ $totals{ $self->account_type }||[] }) { my ($re,$possible_method) = @$total; if ($line =~ /^$re:\s*(.*) Euro$/) { $method = $possible_method; $balance = $1; if ($balance =~ /^(-?[0-9.,]+)$/) { $self->$method( ['????????',$self->parse_amount($balance)]); } else { die "Invalid number '$_' found for $total"; }; }; }; if (! $method) { croak "No summary found in account statement ($line)"; }; }; $lines[0] =~ m!^\s*$! or croak "Expected an empty line after the account balances, got '$lines[0]'"; shift @lines; # Now parse the lines for each cashflow : $lines[0] =~ /^Datum\tWertstellung\tArt/ or croak "Couldn't find start of transactions ($lines[0])"; # Ugly hack for "Art Buchungshinweis" (without a tab :-( ) $lines[0] =~ s/Art Buchungshinweis/Art\tBuchungshinweis/; my (@fields); COLUMN: for my $col (split /\t/, $lines[0]) { for my $target (keys %columns) { if ($col =~ m!^$target$!) { push @fields, $columns{$target}; next COLUMN; }; }; die "Unknown column '$col' in '$lines[0]'"; }; shift @lines; my (%convert) = ( tradedate => \&parse_date, valuedate => \&parse_date, amount => \&parse_amount, ); my @transactions; my $line; for $line (@lines) { next if $line =~ /^\s*$/; my (@row) = split /\t/, $line; scalar @row == scalar @fields or die "Malformed cashflow ($line): Expected ".scalar(@fields)." entries, got ".scalar(@row); for (@row) { s!^\s+!!; s!\s+$!!; }; my (%rec); @rec{@fields} = @row; for (keys %convert) { $rec{$_} = $convert{$_}->($self,$rec{$_}); }; push @transactions, \%rec; }; # Filter the transactions $self->{transactions} = \@transactions; $self }; sub transactions { my ($self,%args) = @_; my ($start_date,$end_date); if (exists $args{on}) { croak "Options 'since'+'upto' and 'on' are incompatible" if (exists $args{since} and exists $args{upto}); croak "Options 'since' and 'on' are incompatible" if (exists $args{since}); croak "Options 'upto' and 'on' are incompatible" if (exists $args{upto}); $args{on} = strftime('%Y%m%d',localtime()) if ($args{on} eq 'today'); $args{on} =~ /^\d{8}$/ or croak "Argument {on => '$args{on}'} dosen't look like a date to me."; $start_date = $args{on} -1; $end_date = $args{on}; } else { $start_date = $args{since} || "00000000"; $end_date = $args{upto} || "99999999"; $start_date =~ /^\d{8}$/ or croak "Argument {since => '$start_date'} dosen't look like a date to me."; $end_date =~ /^\d{8}$/ or croak "Argument {upto => '$end_date'} dosen't look like a date to me."; $start_date < $end_date or croak "The 'since' argument must be less than the 'upto' argument"; }; # Filter the transactions grep { $_->{tradedate} > $start_date and $_->{tradedate} <= $end_date } @{$self->{transactions}}; }; sub value_dates { my ($self) = @_; my %dates; $dates{$_->{valuedate}} = 1 for $self->transactions(); sort keys %dates; }; sub trade_dates { my ($self) = @_; my %dates; $dates{$_->{tradedate}} = 1 for $self->transactions(); sort keys %dates; }; 1; __END__