package Finance::Bank::INGDirect; use strict; use Carp qw(carp croak); use HTTP::Cookies; use LWP::UserAgent; use HTML::Parser; #use Data::Dump qw (dump); our $VERSION = '1.05'; # $Id: INGDirect.pm,v 1.2 2005/12/16 22:30:20 jmrenouard Exp $ # $Log: INGDirect.pm,v $ # Revision 1.2 2005/12/16 22:30:20 jmrenouard # Modification tag TD empéchant la lecture des transactions # # Revision 1.1.1.1 2005/12/16 22:01:09 jmrenouard # Imported sources # =pod =head1 NAME Finance::Bank::INGDirect - Check your "ING Direct France" accounts from Perl =head1 SYNOPSIS use Finance::Bank::INGDirect; my @accounts = Finance::Bank::INGDirect->check_balance( ACN => "167845", PIN => "1234", JOUR => "25", # Day of birthday MOIS => "8", # month of birthday ANNEE => "1952" # year of birthday ); foreach my $account (@accounts) { print "Name: ", $account->name, " Account_no: ", $account->account_no, "\n", "*" x 80, "\n"; print $_->as_string, "\n" foreach $account->statements; } =head1 DESCRIPTION This module provides a read-only interface to the INGDirect online banking system at L. You will need either Crypt::SSLeay installed. The interface of this module is similar to other Finance::Bank::* modules. =head1 WARNING This is code for B, and that means B, and that means B. 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, explicit or implied. =cut =pod =head1 METHODS =head2 new( ACN => "167845", PIN => "1234", JOUR => "25", MOIS => "8", ANNEE => "1952" feedback => sub { warn "Finance::Bank::INGDirect : $_[0]\n" }) Return an object . You can optionally provide to this method a LWP::UserAgent object (argument named "ua"). You can also provide a function used for feedback (useful for verbose mode or debugging) (argument named "feedback") =cut my $urlMain="https://www.ingdirect.fr/secure/general"; my $urlLogin="$urlMain?command=displayLogin"; my $urlContent="$urlMain?command=displayTRAccountSummary"; my $urlAccount="$urlMain?command=goToAccount&account="; my $urlAccount2="$urlMain?command=displayTRHistorique"; sub normalize_number { my ($self,$s) = @_; $s =~ s/ //; $s =~ s/,/./; $s; } sub _parse_content { my ($self, $content) = @_; my ($type, $num, $balance); my $f=0; my $i=0; @{$self->{Accounts}}=(); while ( $content =~ /(.*)\n/g ) { if ( !$f && $1 =~ /class=\"Bleu11\">(.*?)-(.*?)<\/a><\/td>/ ) { $type=$1; $num=$2; $f=1; # print "\n#Found : $type $num"; } if ( $f && $1 =~ /class="Bleu11">(.*)<\/a><\/td>/) { push ( @{$self->{Accounts}}, Finance::Bank::INGDirect::Account->new( $type, $num, $self->normalize_number($1), $self->{ua}, "$urlAccount$i" )); #print "\n\t $type, $num, $1, $urlAccount$i"; $f=0; $i++; } } } sub _get_cookie { my ($self) = @_; $self->{feedback}->("get cookie") if $self->{feedback}; my $cookie_jar = HTTP::Cookies->new; my $response = $self->{ua}->simple_request(HTTP::Request->new(GET => $urlLogin)); $cookie_jar->extract_cookies($response); $self->{ua}->cookie_jar($cookie_jar); } sub _login { my ($self) = @_; $self->{feedback}->("login") if $self->{feedback}; my $request = HTTP::Request->new(POST => $urlMain); $request->content_type('application/x-www-form-urlencoded'); $request->content("ACN=$self->{ACN}&PIN=$self->{PIN}&command=login&locale=fr_FR&device=web&logdatelogin=1&JOUR=$self->{JOUR}&MOIS=$self->{MOIS}&ANNEE=$self->{ANNEE}"); my $response = $self->{ua}->request($request); $response->is_success or die "login failed\n" . $response->error_as_HTML; } sub _list_accounts { my ($self) = @_; $self->{feedback}->("list accounts") if $self->{feedback}; my $response = $self->{ua}->request(HTTP::Request->new(GET => "$urlContent")); $response->is_success or die "can't access account\n" . $response->error_as_HTML; _parse_content($self, $response->content); } sub new { my ($class, %opts) = @_; my $self = bless \%opts, $class; exists $self->{ACN} or croak "Must provide a ACN"; exists $self->{PIN} or croak "Must provide a PIN"; exists $self->{JOUR} or croak "Must provide a JOUR"; exists $self->{MOIS} or croak "Must provide a MOIS"; exists $self->{ANNEE} or croak "Must provide a ANNEE"; $self->{ua} ||= LWP::UserAgent->new; _get_cookie($self); _login($self); _list_accounts($self); $self; } sub default_account { my ($self) = @_; return $self->{Accounts}[0]; } =pod =head2 check_balance( ACN => "167845", PIN => "1234", JOUR => "25", MOIS => "8", ANNEE => "1952" feedback => sub { warn "Finance::Bank::INGDirect : $_[0]\n" }) Return a list of account (F::B::INGDirect::Account) objects, one for each of your bank accounts. =cut sub check_balance { my $self = &new; @{$self->{Accounts}}; } package Finance::Bank::INGDirect::Account; use Data::Dump qw (dump); =pod =head1 Account methods =head2 type( ) Returns the human-readable name of the account. =head2 account_no( ) Return the account number, in the form C<0123456L012>. =head2 balance( ) Returns the balance of the account. =head2 statements( ) Return a list of Statement object (Finance::Bank::INGDirect::Statement). =head2 currency( ) Returns the currency of the account as a three letter ISO code (EUR, CHF,etc.). =cut sub new { my ($class, $type, $num, $bal, $ua, $url) = @_; my %account; $account{type}=$type; $account{account_no}=$num; $account{balance}=$bal; $account{ua}=$ua; $account{url}=$url; $account{statements}=(); my $self2 = bless \%account, $class; $self2; } sub type { $_[0]->{type} } sub account_no { $_[0]->{account_no} } sub balance { $_[0]->{balance} } sub currency { 'EUR' } my $response; sub statements { my ($self) = @_; $self->{url} or return; unless (defined @{$self->{statements}}) { $self->{feedback}->("get statements") if $self->{feedback}; my $response = $self->{ua}->request(HTTP::Request->new(GET => $self->{url})); $response->is_success or die "can't access account $self->{url} statements\n" . $response->error_as_HTML; $response = $self->{ua}->request(HTTP::Request->new(GET => $urlAccount2)); $response->is_success or die "can't access account $urlAccount2 statements\n" . $response->error_as_HTML; _parse_content_account($self, $response->content); }; @{$self->{statements}}; } sub normalize_number { my ($self, $s) = @_; $s =~ s/ //; $s =~ s/,/./; $s; } sub _parse_content_account { my ($self, $content)=@_; #Parsing html content while ( $content =~ /BgdTabOra\">(\d+\/\d+\/\d+)<\/TD>(.|\n)+?Bleu11\">(.*?)<\/span>(.|\n)+?BgdTabOra" align="right">(.*?)<\/TD>/g) { #print "\n# $1 $3 $5"; push (@{$self->{statements}}, Finance::Bank::INGDirect::Statement->new ($1, $3, $self->normalize_number($5))); } } package Finance::Bank::INGDirect::Statement; =pod =head1 Statement methods =head2 date( ) Returns the date when the statement occured, in DD/MM/YY format. =head2 description( ) Returns a brief description of the statement. =head2 amount( ) Returns the amount of the statement (expressed in Euros or the account's currency). Although the Crédit Mutuel website displays number in continental format (i.e. with a coma as decimal separator), amount() returns a real number. =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, $date, $description, $amount) = @_; my %stat; $stat{date}=$date; $stat{description}=$description; $stat{amount}=$amount; bless \%stat, $class; } sub description { $_[0]{description} } sub amount { $_[0]{amount} } sub date { $_[0]{date} } sub as_string { my ($self, $separator) = @_; join($separator || "\t", $self->{date}, $self->{description}, $self->{amount}); } 1; =pod =head1 COPYRIGHT Copyright 2005, Jean-Marie Renouard. All Rights Reserved. This module can be redistributed under the same terms as Perl itself. =head1 AUTHOR Thanks to Pixel for Finance::Bank::LaPoste, Cédric Bouvier for Finance::Bank::CreditMut (and also to Simon Cozens and Briac Pilpré for various Finance::Bank::*) =head1 SEE ALSO Finance::Bank::BNPParibas, Finance::Bank::CreditMut, Finance::Bank::LaPoste, ... =cut