File Coverage

blib/lib/Finance/Bank/CreditMut.pm
Criterion Covered Total %
statement 17 88 19.3
branch 0 16 0.0
condition 0 19 0.0
subroutine 6 19 31.5
pod 1 1 100.0
total 24 143 16.7


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