File Coverage

blib/lib/Finance/Bank/CreditMut.pm
Criterion Covered Total %
statement 15 85 17.6
branch 0 16 0.0
condition 0 19 0.0
subroutine 5 18 27.7
pod 1 1 100.0
total 21 139 15.1


line stmt bran cond sub pod time code
1             package Finance::Bank::CreditMut;
2 1     1   7174 use strict;
  1         5  
  1         40  
3 1     1   6 use Carp qw(carp croak);
  1         2  
  1         59  
4 1     1   3845 use WWW::Mechanize;
  1         240495  
  1         50  
5 1     1   1257 use HTML::TableExtract;
  1         8507  
  1         10  
6 1     1   51 use vars qw($VERSION);
  1         2  
  1         1309  
7              
8             $VERSION = 0.12;
9              
10             =pod
11              
12             =head1 NAME
13              
14             Finance::Bank::CreditMut - Check your Crédit Mutuel accounts from Perl
15              
16             =head1 SYNOPSIS
17              
18             use Finance::Bank::CreditMut;
19              
20             my @accounts = Finance::Bank::CreditMut->check_balance(
21             username => "$username", # Be sure to put the numbers
22             password => "$password", # between quote.
23             );
24              
25             foreach my $account ( @accounts ){
26             local $\ = "\n";
27             print " Name ", $account->name;
28             print " Account_no ", $account->account_no;
29             print " Statement\n";
30              
31             foreach my $statement ( $account->statements ){
32             print $statement->as_string;
33             }
34             }
35              
36             =head1 DESCRIPTION
37              
38             This module provides a rudimentary interface to the CyberMut online banking
39             system at L. You will need either
40             Crypt::SSLeay or IO::Socket::SSL installed for HTTPS support to work with
41             LWP.
42              
43             The interface of this module is directly taken from Briac Pilpré's
44             Finance::Bank::BNPParibas.
45              
46             =head1 WARNING
47              
48             This is code for B, and that means B, and that
49             means B. You are encouraged, nay, expected, to audit the source
50             of this module yourself to reassure yourself that I am not doing anything
51             untoward with your banking data. This software is useful to me, but is
52             provided under B, explicit or implied.
53              
54             =head1 METHODS
55              
56             =head2 check_balance( username => $username, password => $password, ua => $ua )
57              
58             Return a list of account (F::B::CM::Account) objects, one for each of your
59             bank accounts. You can provide to this method a WWW::Mechanize object as
60             third argument. If not, a new one will be created.
61              
62             =cut
63              
64             sub check_balance {
65 0     0 1   my ( $class, %opts ) = @_;
66 0 0         croak "Must provide a password" unless exists $opts{password};
67 0 0         croak "Must provide a username" unless exists $opts{username};
68              
69 0           my @accounts;
70              
71 0   0       $opts{ua} ||= WWW::Mechanize->new(
72             agent => __PACKAGE__ . "/$VERSION ($^O)",
73             cookie_jar => {},
74             );
75              
76 0           my $self = bless {%opts}, $class;
77              
78 0           my $orig_r;
79 0           my $count = 0;
80             {
81 0           $orig_r = $self->{ua}->get("https://www.creditmutuel.fr/");
  0            
82             # loop detected, try again
83 0           ++$count;
84 0 0 0       redo unless $orig_r->content || $count > 13;
85             }
86 0 0         croak $orig_r->error_as_HTML if $orig_r->is_error;
87              
88             {
89 0           local $^W; # both fields_are read-only
  0            
90 0           my $click_r = $self->{ua}->submit_form(
91             form_number => 1,
92             fields => {
93             _cm_user => $self->{username},
94             _cm_pwd => $self->{password},
95             }
96             );
97 0 0         croak $click_r->error_as_HTML if $click_r->is_error;
98             }
99              
100 0           my $r = $self->{ua}->follow_link(url_regex => qr/situation_financiere\.cgi/);
101 0 0         croak $r->error_as_HTML if $r->is_error;
102            
103             # The current page contains a table displaying the accounts and their
104             # balances.
105              
106 0           my $te = new HTML::TableExtract(headers => [
107             q{Compte},
108             q{D.bit},
109             q{Cr.dit},
110             ]);
111 0           $te->parse($self->{ua}->content());
112 0           for my $ts ( $te->table_states() ) {
113 0           foreach ( $ts->rows() ) {
114             # The name actually also contains the account number.
115             # Finance::Bank::CreditMutuel::Account::new will take care of
116             # splitting.
117 0           my ($name, $dept, $asset) = @$_;
118 0           for ($name, $dept, $asset) {
119 0           s/^\s+|\s+$//g; # remove leading and trailing whitespace
120 0           s/\s+/ /g; # collapse all whitespace to one single blank
121             }
122 0           my $link = $self->{ua}->find_link(text_regex => qr/$name/);
123              
124             # we only care about accounts that are displayed with
125             # 'mouvements.cgi' (mortgages use another page that does not
126             # provide CSV downloads. Maybe a future version will handle
127             # this)
128 0 0 0       next unless $link && $link->[0] =~ /mouvements\.cgi/;
129 0           for ($dept, $asset) {
130 0           tr/,/./;
131 0           tr/-+.A-Z0-9//cd;
132             }
133             # Negative and positive balances are displayed in different
134             # columns: take either one and split the currency code at the
135             # same time.
136 0   0       my ($balance,$currency) = $dept || $asset =~ /(.*?)([A-Z]+)$/;
137 0           $balance += 0; # turn string into a number
138 0           push @accounts, Finance::Bank::CreditMut::Account->new(
139             $name,
140             $currency,
141             $balance,
142             $self->{ua},
143             $self->{ua}->base . q{/../} . $link->[0],
144             );
145             }
146             }
147 0           @accounts;
148             }
149              
150             package Finance::Bank::CreditMut::Account;
151              
152             =pod
153              
154             =head1 Account methods
155              
156             =head2 sort_code()
157              
158             Return the sort code of the account. Currently, it returns an undefined
159             value.
160              
161             =head2 name()
162              
163             Returns the human-readable name of the account.
164              
165             =head2 account_no()
166              
167             Return the account number, in the form C, where X and Y are
168             numbers.
169              
170             =head2 balance()
171              
172             Returns the balance of the account.
173              
174             =head2 statements()
175              
176             Return a list of Statement object (Finance::Bank::CreditMut::Statement).
177              
178             =head2 currency()
179              
180             Returns the currency of the account as a three letter ISO code (EUR, CHF,
181             etc.)
182              
183             =cut
184              
185             sub new {
186 0     0     my $class = shift;
187 0           my ($name, $currency, $balance, $ua, $url) = @_;
188 0 0         $name =~ /(\d+.\d+(?:.\d+)?)\s+(.*)/ or warn "!!";
189 0           (my $account_no, $name) = ($1, $2);
190 0           $account_no =~ s/\D/ /g; # remove non-breaking space.
191 0           $account_no =~ s/^\d+.//; # remove leading agency number
192              
193 0           bless {
194             name => $name,
195             account_no => $account_no,
196             sort_code => undef,
197             date => undef,
198             balance => $balance,
199             currency => $currency,
200             ua => $ua,
201             url => $url,
202             }, $class;
203             }
204              
205 0     0     sub sort_code { undef }
206 0     0     sub name { $_[0]->{name} }
207 0     0     sub account_no { $_[0]->{account_no} }
208 0     0     sub balance { $_[0]->{balance} }
209 0     0     sub currency { $_[0]->{currency} }
210             sub statements {
211              
212 0     0     my $self = shift;
213              
214             @{
215 0   0       $self->{statements} ||= do {
  0            
216 0           $self->{ua}->get($self->{url});
217 0           $self->{ua}->follow_link(text_regex => qr/XP/);
218 0           chomp(my @content = split /\015\012/, $self->{ua}->content());
219 0           shift @content;
220 0           [map Finance::Bank::CreditMut::Statement->new($_), @content];
221             };
222             };
223             }
224              
225             package Finance::Bank::CreditMut::Statement;
226              
227             =pod
228              
229             =head1 Statement methods
230              
231             =head2 date()
232              
233             Returns the date when the statement occured, in DD/MM/YY format.
234              
235             =head2 description()
236              
237             Returns a brief description of the statement.
238              
239             =head2 amount()
240              
241             Returns the amount of the statement (expressed in Euros or the account's
242             currency). Although the Crédit Mutuel website displays number in continental
243             format (i.e. with a coma as decimal separator), amount() returns a real
244             number.
245              
246             =head2 as_string($separator)
247              
248             Returns a tab-delimited representation of the statement. By default, it uses
249             a tabulation to separate the fields, but the user can provide its own
250             separator.
251              
252             =cut
253              
254             sub new {
255 0     0     my $class = shift;
256 0           my $statement = shift;
257              
258 0           my ($date, undef, $withdrawal, $deposit, $comment) = split /;/, $statement;
259              
260 0           $date =~ s/\d\d(\d\d)$/$1/; # year on 2 digits only
261             # negative number are displayed in a separate column. Move them to the same
262             # one as positive numbers.
263 0   0       my $amount = $withdrawal || $deposit || 0;
264 0           $amount =~ s/,/./;
265 0           $amount =~ tr/'//d; # remove thousand separators
266 0           $amount += 0; # turn into a number
267              
268 0           bless [ $date, $comment, $amount ], $class;
269             }
270              
271 0     0     sub date { $_[0]->[0] }
272 0     0     sub description { $_[0]->[1] }
273 0     0     sub amount { $_[0]->[2] }
274              
275 0   0 0     sub as_string { join ( $_[1] || "\t", @{ $_[0] } ) }
  0            
276              
277             1;
278              
279             __END__