package Finance::Bank::BNPParibas;
use strict;
use Carp qw(carp croak);
use WWW::Mechanize;
#use LWP::Debug qw(+);
use vars qw($VERSION);
$VERSION = 0.07;
use constant BASE_URL => 'https://www.secure.bnpparibas.net/controller?type=auth';
use constant LOGIN_FORM_NAME => 'logincanalnet';
=pod
=head1 NAME
Finance::Bank::BNPParibas - Check your BNP bank accounts from Perl
=head1 SYNOPSIS
use Finance::Bank::BNPParibas;
my @accounts = Finance::Bank::BNPParibas->check_balance(
username => "$username", # Be sure to put the numbers
password => "$password", # between quote.
);
foreach my $account ( @accounts ){
local $\ = "\n";
print " Name ", $account->name;
print " Account_no ", $account->account_no;
print " Balance ", $account->balance;
print " Statement\n";
foreach my $statement ( $account->statements ){
print $statement->as_string;
}
}
=head1 DESCRIPTION
This module provides a rudimentary interface to the BNPNet online
banking system at L<https://www.bnpnet.bnp.fr/>. You will need
either Crypt::SSLeay or IO::Socket::SSL installed for HTTPS support
to work with LWP.
The interface of this module is directly taken from Simon Cozens'
Finance::Bank::LloydsTSB.
=head1 WARNING
This is code for B<online banking>, and that means B<your money>, and
that means B<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 B<NO GUARANTEE>, explicit or implied.
=head1 METHODS
=head2 check_balance( username => $username, password => $password, ua => $ua )
Return a list of account (F::B::B::Account) objects, one for each of
your bank accounts. You can provide to this method a WWW::Mechanize
object as third argument.
=cut
sub check_balance {
my ( $class, %opts ) = @_;
croak "Must provide a password" unless exists $opts{password};
croak "Must provide a username" unless exists $opts{username};
my @accounts;
$opts{ua} ||= WWW::Mechanize->new(
agent => "Finance::Bank::BNPParibas/$VERSION ($^O)",
cookie_jar => {},
);
my $self = bless {%opts}, $class;
my $orig_r;
my $count = 0;
{
$orig_r = $self->{ua}->get(BASE_URL);
# loop detected, try again
++$count;
redo unless $orig_r->content || $count > 13;
}
croak $orig_r->error_as_HTML if $orig_r->is_error;
# Check if the login form is in the page.
$self->{ua}->quiet(1);
$self->{ua}->form_name(LOGIN_FORM_NAME)
or croak "Cannot find the login form '" . LOGIN_FORM_NAME . "'";
# XXX Because the input below are created with javascript, we
# have to manually add them to the Form.
# see http://rt.cpan.org/NoAuth/Bug.html?id=2940
$self->{ua}->{form}
->push_input( "text", { type => "text", name => "userid", value => "" } );
$self->{ua}->{form}
->push_input( "text", { type => "text", name => "password", value => "" } );
$self->{ua}->set_fields(
userid => $self->{username},
password => $self->{password},
);
my $click_r = $self->{ua}->submit;
$self->{ua}->quiet(0);
croak $click_r->error_as_HTML if $click_r->is_error;
# XXX Without this header, bnpnet won't send the next page.
$self->{ua}->add_header( Accept => 'text/html' );
$self->{ua}->get('/SAF_TLC');
# Check if the 100 login limit is reached:
if ( $self->{ua}->content =~ /Code erreur=13/ ){
carp "Trying to login more than 100 times with the same password\n";
# SAF_CHM is the page to chang password
$self->{ua}->get('/SAF_CHM');
my @numbers = ( 0 .. 9 );
my $temp_password = join ( '', @numbers[ map { rand @numbers } ( 1 .. 6 ) ] );
carp "temp password: '$temp_password'\n";
$self->{ua}->set_fields(
ch1 => $self->{password},
ch2 => $temp_password,
ch3 => $temp_password,
);
$self->{ua}->submit;
$self->{ua}->get('/SAF_CHM');
$self->{ua}->set_fields(
ch1 => $temp_password,
ch2 => $self->{password},
ch3 => $self->{password},
);
$self->{ua}->submit;
$self->{ua}->get('/SAF_TLC');
}
# Check if the account download form is in the page.
$self->{ua}->quiet(1);
$self->{ua}->form_number(1)
or croak "Cannot find the account download form";
$self->{ua}->quiet(0);
# If there is only one account, no radio button is present in the form.
# We need to add one manually.
# see http://rt.cpan.org/Ticket/Display.html?id=3156
unless ( $self->{ua}->{form}->find_input( "ch_rop", "radio" ) ) {
$self->{ua}->{form}
->push_input( "radio", { type => "radio", name => "ch_rop", value => "tous" } );
}
$self->{ua}->set_fields(
ch_rop => 'tous',
ch_rop_fmt_fic => 'RTEXC',
ch_rop_fmt_dat => 'JJMMAA',
ch_rop_fmt_sep => 'VG',
ch_rop_dat => 'tous',
ch_rop_dat_deb => '',
ch_rop_dat_fin => '',
ch_memo => 'OUI',
);
$self->{ua}->submit;
foreach ( @{ $self->{ua}->{links} } ) {
my $qif = $_->[0];
next unless $qif =~ /\.exl$/;
my $qif_r = $self->{ua}->get($qif);
carp $qif_r->error_as_HTML if $qif_r->is_error;
next
if $self->{ua}->{content} =~
/<html>/i; # no operation for this account
push @accounts,
Finance::Bank::BNPParibas::Account->new( $self->{ua}->content );
}
@accounts;
}
# The format of the date from BNPNet is DD/MM/YY, so we have to transform it to
# an ISO format: YYYY-MM-DD
sub _normalize_date {
my $date = shift;
my ( $d, $m, $y ) = split ( /\//, $date );
$y = $y =~ /^[789]\d$/ ? $y + 1900 : $y + 2000;
return "$y-$m-$d";
}
package Finance::Bank::BNPParibas::Account;
=pod
=head1 Account methods
=head2 sort_code()
Return the sort code of the account. Currently, it returns an
undefined value.
=head2 name()
Returns the human-readable name of the account.
=head2 account_no()
Return the account number, in the form C<XXX YYYYYYYYY ZZ>, where X, Y
and Z are numbers.
=head2 balance()
Returns the balance of the account. Note that the BNP site displays them
in French format (i.e C<123,75>), but the string returns a number perl
understands (i.e C<123.75>).
=head2 statements()
Return a list of Statement object (Finance::Bank::BNPParibas::Statement).
=cut
sub new {
my $class = shift;
chomp( my @content = split ( /\n/, shift ) );
my $header = shift @content;
my ( $name, $account_no, $date, $balance ) =
( $header =~
m/^(.+)\s+(\d{5}\s+\d{9}\s+\d{2})\t+(\d{2}\/\d{2}\/\d{2})\t+(\d+,\d+)/
);
$balance =~ s/,/./;
my @statements;
push @statements,
Finance::Bank::BNPParibas::Statement->new($_) foreach @content;
$date = Finance::Bank::BNPParibas::_normalize_date($date);
bless {
name => $name,
account_no => $account_no,
sort_code => undef,
date => $date,
balance => $balance,
statements => [@statements],
}, $class;
}
sub sort_code { undef }
sub name { $_[0]->{name} }
sub account_no { $_[0]->{account_no} }
sub balance { $_[0]->{balance} }
sub statements { @{ $_[0]->{statements} } }
package Finance::Bank::BNPParibas::Statement;
=pod
=head1 Statement methods
=head2 date()
Returns the date when the statement occured, in YYYY-MM-DD format.
=head2 value_date()
Returns the date the transfer entry to an account is considered
effective, in YYYY-MM-DD format.
=head2 description()
Returns a brief description of the statement.
=head2 amount()
Returns the amount of the statement (expressed in Euros).
=head2 as_string($separator)
Returns a tab-delimited representation of the statement. By default, it
uses a tabulation to separate the fields, but the user can provide its
own separator.
=cut
sub new {
my $class = shift;
my $statement = shift;
my @entry = split ( /\t/, $statement );
pop @entry;
my $self = {};
$self->{date} = Finance::Bank::BNPParibas::_normalize_date( $entry[0] );
$entry[1] =~ s/\s+/ /g;
$self->{description} = $entry[1];
if ( scalar @entry == 3 ) {
$entry[2] =~ s/,/./;
$self->{amount} = $entry[2];
}
else {
$self->{value_date} =
Finance::Bank::BNPParibas::_normalize_date( $entry[2] );
$entry[3] =~ s/,/./;
$self->{amount} = $entry[3];
}
bless $self, $class;
}
sub date { $_[0]->{date} }
sub value_date { $_[0]->{value_date} }
sub description { $_[0]->{description} }
sub amount { $_[0]->{amount} }
sub as_string {
join ( $_[1] || "\t", $_[0]->{date}, $_[0]->{description}, ($_[0]->{value_date} ||''), $_[0]->{amount} )
}
1;
__END__
=head1 BUGS
Please report any bugs or comments using the Request Tracker interface:
L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Finance-Bank-BNPParibas>
=head1 COPYRIGHT
Copyright 2002-2003, Briac Pilpré. All Rights Reserved. This module can be
redistributed under the same terms as Perl itself.
=head1 AUTHOR
Briac Pilpré <briac@cpan.org>
Thanks to Simon Cozens for releasing Finance::Bank::LloydsTSB
=head1 SEE ALSO
Finance::Bank::LloydsTSB, WWW::Mechanize
=cut