File Coverage

blib/lib/Finance/Bank/BNPParibas.pm
Criterion Covered Total %
statement 18 108 16.6
branch 0 28 0.0
condition 0 10 0.0
subroutine 6 20 30.0
pod 1 1 100.0
total 25 167 14.9


line stmt bran cond sub pod time code
1             package Finance::Bank::BNPParibas;
2 1     1   6288 use strict;
  1         4  
  1         45  
3 1     1   6 use Carp qw(carp croak);
  1         2  
  1         66  
4 1     1   1384 use WWW::Mechanize;
  1         497374  
  1         44  
5              
6             #use LWP::Debug qw(+);
7 1     1   9 use vars qw($VERSION);
  1         1  
  1         61  
8              
9             $VERSION = 0.09;
10              
11 1     1   6 use constant BASE_URL => 'https://www.secure.bnpparibas.net/controller?type=homeconnex';
  1         2  
  1         64  
12 1     1   5 use constant LOGIN_FORM_NAME => 'logincanalnet';
  1         2  
  1         1703  
13              
14             =pod
15              
16             =head1 NAME
17              
18             Finance::Bank::BNPParibas - Check your BNP bank accounts from Perl
19              
20             =head1 SYNOPSIS
21              
22             use Finance::Bank::BNPParibas;
23              
24             my @accounts = Finance::Bank::BNPParibas->check_balance(
25             username => "$username", # Be sure to put the numbers
26             password => "$password", # between quote.
27             );
28              
29             foreach my $account ( @accounts ){
30             local $\ = "\n";
31             print " Name ", $account->name;
32             print " Account_no ", $account->account_no;
33             print " Balance ", $account->balance;
34             print " Statement\n";
35              
36             foreach my $statement ( $account->statements ){
37             print $statement->as_string;
38             }
39             }
40              
41             =head1 DESCRIPTION
42              
43             This module provides a rudimentary interface to the BNPNet online
44             banking system at L. You will need
45             either Crypt::SSLeay or IO::Socket::SSL installed for HTTPS support
46             to work with LWP.
47              
48             The interface of this module is directly taken from Simon Cozens'
49             Finance::Bank::LloydsTSB.
50              
51             =head1 WARNING
52              
53             This is code for B, and that means B, and
54             that means B. You are encouraged, nay, expected, to audit
55             the source of this module yourself to reassure yourself that I am not
56             doing anything untoward with your banking data. This software is useful
57             to me, but is provided under B, explicit or implied.
58              
59             =head1 METHODS
60              
61             =head2 check_balance( username => $username, password => $password, ua => $ua )
62              
63             Return a list of account (F::B::B::Account) objects, one for each of
64             your bank accounts. You can provide to this method a WWW::Mechanize
65             object as third argument.
66              
67             =cut
68              
69             sub check_balance {
70 0     0 1   my ( $class, %opts ) = @_;
71 0 0         croak "Must provide a password" unless exists $opts{password};
72 0 0         croak "Must provide a username" unless exists $opts{username};
73              
74 0           my @accounts;
75              
76 0   0       $opts{ua} ||= WWW::Mechanize->new(
77             agent => "Finance::Bank::BNPParibas/$VERSION ($^O)",
78             cookie_jar => {},
79             );
80              
81 0           my $self = bless {%opts}, $class;
82              
83 0           my $orig_r;
84 0           my $count = 0;
85             {
86 0           $orig_r = $self->{ua}->get(BASE_URL);
  0            
87              
88             # loop detected, try again
89 0           ++$count;
90 0 0 0       redo unless $orig_r->content || $count > 13;
91             }
92 0 0         croak $orig_r->error_as_HTML if $orig_r->is_error;
93              
94             # As of 2005-04-19, BNP changed their default login form to a fancy
95             # imagemap to compose the password, thankfully, they still provide
96             # access to the old login form:
97 0           $self->{ua}->follow_link( url_regex => qr/identifiant=secure_bnpparibas_net/ );
98              
99             # Check if the login form is in the page.
100 0           $self->{ua}->quiet(1);
101 0 0         $self->{ua}->form_name(LOGIN_FORM_NAME)
102             or croak "Cannot find the login form '" . LOGIN_FORM_NAME . "'";
103              
104 0           $self->{ua}->set_fields(
105             login => $self->{username},
106             password => $self->{password},
107             );
108              
109 0           my $click_r = $self->{ua}->submit;
110              
111 0           $self->{ua}->quiet(0);
112 0 0         croak $click_r->error_as_HTML if $click_r->is_error;
113              
114             # XXX Without this header, bnpnet won't send the next page.
115 0           $self->{ua}->add_header( Accept => 'text/html' );
116              
117 0           $self->{ua}->get('/SAF_TLC');
118              
119             # Check if the 100 login limit is reached:
120 0 0         if ( $self->{ua}->content =~ /Code erreur=13/ ){
121 0           carp "Trying to login more than 100 times with the same password\n";
122              
123             # SAF_CHM is the page to chang password
124 0           $self->{ua}->get('/SAF_CHM');
125              
126 0           my @numbers = ( 0 .. 9 );
127 0           my $temp_password = join ( '', @numbers[ map { rand @numbers } ( 1 .. 6 ) ] );
  0            
128              
129 0           carp "temp password: '$temp_password'\n";
130            
131 0           $self->{ua}->set_fields(
132             ch1 => $self->{password},
133             ch2 => $temp_password,
134             ch3 => $temp_password,
135             );
136 0           $self->{ua}->submit;
137              
138 0           $self->{ua}->get('/SAF_CHM');
139 0           $self->{ua}->set_fields(
140             ch1 => $temp_password,
141             ch2 => $self->{password},
142             ch3 => $self->{password},
143             );
144 0           $self->{ua}->submit;
145            
146 0           $self->{ua}->get('/SAF_TLC');
147             }
148              
149            
150             # Check if the account download form is in the page.
151 0           $self->{ua}->quiet(1);
152 0 0         $self->{ua}->form_number(1)
153             or croak "Cannot find the account download form";
154 0           $self->{ua}->quiet(0);
155              
156             # If there is only one account, no radio button is present in the form.
157             # We need to add one manually.
158             # see http://rt.cpan.org/Ticket/Display.html?id=3156
159 0 0         unless ( $self->{ua}->{form}->find_input( "ch_rop", "radio" ) ) {
160 0           $self->{ua}->{form}
161             ->push_input( "radio", { type => "radio", name => "ch_rop", value => "tous" } );
162             }
163            
164 0           $self->{ua}->set_fields(
165             ch_rop => 'tous',
166             ch_rop_fmt_fic => 'RTEXC',
167             ch_rop_fmt_dat => 'JJMMAA',
168             ch_rop_fmt_sep => 'VG',
169             ch_rop_dat => 'tous',
170             ch_rop_dat_deb => '',
171             ch_rop_dat_fin => '',
172             ch_memo => 'OUI',
173             );
174            
175 0           $self->{ua}->submit;
176              
177 0           foreach ( @{ $self->{ua}->{links} } ) {
  0            
178 0           my $qif = $_->[0];
179 0 0         next unless $qif =~ /\.exl$/;
180              
181 0           my $qif_r = $self->{ua}->get($qif);
182 0 0         carp $qif_r->error_as_HTML if $qif_r->is_error;
183              
184             next
185 0 0         if $self->{ua}->{content} =~
186             //i; # no operation for this account
187 0           push @accounts,
188             Finance::Bank::BNPParibas::Account->new( $self->{ua}->content );
189             }
190 0           @accounts;
191             }
192              
193             # The format of the date from BNPNet is DD/MM/YY, so we have to transform it to
194             # an ISO format: YYYY-MM-DD
195             sub _normalize_date {
196 0     0     my $date = shift;
197 0           my ( $d, $m, $y ) = split ( /\//, $date );
198 0 0         $y = $y =~ /^[789]\d$/ ? $y + 1900 : $y + 2000;
199 0           return "$y-$m-$d";
200             }
201              
202             package Finance::Bank::BNPParibas::Account;
203              
204             =pod
205              
206             =head1 Account methods
207              
208             =head2 sort_code()
209              
210             Return the sort code of the account. Currently, it returns an
211             undefined value.
212              
213             =head2 name()
214              
215             Returns the human-readable name of the account.
216              
217             =head2 account_no()
218              
219             Return the account number, in the form C, where X, Y
220             and Z are numbers.
221              
222             =head2 balance()
223              
224             Returns the balance of the account. Note that the BNP site displays them
225             in French format (i.e C<123,75>), but the string returns a number perl
226             understands (i.e C<123.75>).
227              
228             =head2 statements()
229              
230             Return a list of Statement object (Finance::Bank::BNPParibas::Statement).
231              
232             =cut
233              
234             sub new {
235 0     0     my $class = shift;
236 0           chomp( my @content = split ( /\n/, shift ) );
237 0           my $header = shift @content;
238              
239 0           my ( $name, $account_no, $date, $balance ) =
240             ( $header =~
241             m/^(.+)\s+(\d{5}\s+\d{9}\s+\d{2})\t+(\d{2}\/\d{2}\/\d{2})\t+(\d+,\d+)/
242             );
243              
244 0           $balance =~ s/,/./;
245              
246 0           my @statements;
247             push @statements,
248 0           Finance::Bank::BNPParibas::Statement->new($_) foreach @content;
249              
250 0           $date = Finance::Bank::BNPParibas::_normalize_date($date);
251              
252 0           bless {
253             name => $name,
254             account_no => $account_no,
255             sort_code => undef,
256             date => $date,
257             balance => $balance,
258             statements => [@statements],
259             }, $class;
260             }
261              
262 0     0     sub sort_code { undef }
263 0     0     sub name { $_[0]->{name} }
264 0     0     sub account_no { $_[0]->{account_no} }
265 0     0     sub balance { $_[0]->{balance} }
266 0     0     sub statements { @{ $_[0]->{statements} } }
  0            
267              
268             package Finance::Bank::BNPParibas::Statement;
269              
270             =pod
271              
272             =head1 Statement methods
273              
274             =head2 date()
275              
276             Returns the date when the statement occured, in YYYY-MM-DD format.
277              
278             =head2 value_date()
279              
280             Returns the date the transfer entry to an account is considered
281             effective, in YYYY-MM-DD format.
282              
283             =head2 description()
284              
285             Returns a brief description of the statement.
286              
287             =head2 amount()
288              
289             Returns the amount of the statement (expressed in Euros).
290              
291             =head2 as_string($separator)
292              
293             Returns a tab-delimited representation of the statement. By default, it
294             uses a tabulation to separate the fields, but the user can provide its
295             own separator.
296              
297             =cut
298              
299             sub new {
300 0     0     my $class = shift;
301 0           my $statement = shift;
302              
303 0           my @entry = split ( /\t/, $statement );
304              
305 0           pop @entry;
306              
307 0           my $self = {};
308              
309 0           $self->{date} = Finance::Bank::BNPParibas::_normalize_date( $entry[0] );
310 0           $entry[1] =~ s/\s+/ /g;
311 0           $self->{description} = $entry[1];
312 0 0         if ( scalar @entry == 3 ) {
313 0           $entry[2] =~ s/,/./;
314 0           $self->{amount} = $entry[2];
315             }
316             else {
317 0           $self->{value_date} =
318             Finance::Bank::BNPParibas::_normalize_date( $entry[2] );
319 0           $entry[3] =~ s/,/./;
320 0           $self->{amount} = $entry[3];
321             }
322              
323 0           bless $self, $class;
324             }
325              
326 0     0     sub date { $_[0]->{date} }
327 0     0     sub value_date { $_[0]->{value_date} }
328 0     0     sub description { $_[0]->{description} }
329 0     0     sub amount { $_[0]->{amount} }
330              
331             sub as_string {
332 0   0 0     join ( $_[1] || "\t", $_[0]->{date}, $_[0]->{description}, ($_[0]->{value_date} ||''), $_[0]->{amount} )
      0        
333             }
334              
335             1;
336              
337             __END__