File Coverage

blib/lib/Finance/Bank/NetBranch.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Finance::Bank::NetBranch - Manage your NetBranch accounts with Perl
4              
5             =cut
6             package Finance::Bank::NetBranch;
7              
8 1     1   23901 use strict;
  1         3  
  1         42  
9 1     1   5 use warnings;
  1         3  
  1         31  
10              
11 1     1   440 use Alias 'attr';
  0            
  0            
12             use Carp;
13             use Date::Parse;
14             use DateTime;
15             use HTML::Entities qw(%entity2char _decode_entities);
16             use HTML::TreeBuilder;
17             use WWW::Mechanize;
18             $Alias::AttrPrefix = "main::"; # make use strict 'vars' palatable
19              
20             =head1 VERSION
21              
22             Version 0.07
23              
24             =cut
25              
26             our $VERSION = 0.07;
27              
28             =head1 SYNOPSIS
29              
30             use Finance::Bank::NetBranch;
31             my $nb = Finance::Bank::NetBranch->new(
32             url => 'https://nbp1.cunetbranch.com/valley/',
33             account => '12345',
34             password => 'abcdef',
35             );
36              
37             my @accounts = $nb->accounts;
38              
39             foreach (@accounts) {
40             printf "%20s : %8s : USD %9.2f of %9.2f\n",
41             $_->name, $_->account_no, $_->available, $_->balance;
42             my $days = 20;
43             for ($_->transactions(from => time - (86400 * $days), to => time)) {
44             printf "%10s | %20s | %80s : %9.2f, %9.2f\n",
45             $_->date->ymd, $_->type, $_->description, $_->amount, $_->balance;
46             }
47             }
48              
49             =head1 DESCRIPTION
50              
51             This module provides a rudimentary interface to NetBranch online banking. This
52             module was originally implemented to interface with Valley Communities Credit
53             Union's page at C, but the behavior of
54             the module is theoretically generalized to "NetBranch" type online access.
55             However, I do not have access to another NetBranch account with another bank,
56             and so any feedback on the actual behavior of this module would be greatly
57             appreciated.
58              
59             You will need either C or C installed for HTTPS
60             support to work.
61              
62             =head1 CLASS METHODS
63              
64             =head2 Finance::Bank::NetBranch
65              
66             =over 4
67              
68             =item new
69              
70             Creates a new C object; does not connect to the server.
71              
72             =cut
73             sub new {
74             my $type = shift;
75             bless { @_ }, $type;
76             }
77              
78             =back
79              
80             =head1 OBJECT METHODS
81              
82             =head2 Finance::Bank::NetBranch
83              
84             =over 4
85              
86             =item accounts
87              
88             Retrieves cached accounts information, connecting to the server if necessary.
89              
90             =cut
91             sub accounts {
92             my $self = attr shift;
93             @::accounts || @{ $self->_get_balances }
94             }
95              
96             =item _login
97              
98             Logs into the NetBranch site (internal use only)
99              
100             =cut
101             sub _login {
102             my $self = attr shift;
103              
104             $::mech ||= WWW::Mechanize->new;
105             $::mech->get($::url)
106             or die "Could not fetch login page URL '$::url'";
107             my $result = $::mech->submit_form(
108             form_name => 'frmLogin',
109             fields => {
110             USERNAME => $::account,
111             password => $::password,
112             },
113             button => 'Login'
114             ) or die "Could not submit login form as account '$::account'";
115              
116             $::mech->uri =~ /welcome/i
117             or die "Failed to log in as account '$::account'";
118              
119             $::logged_in = 1;
120             $result;
121             }
122              
123             =item _logout
124              
125             Logs out of the NetBranch site (internal use only)
126              
127             =cut
128             sub _logout {
129             my $self = attr shift;
130             $::mech->follow_link(text_regex => qr/Logo(ut|ff)/i)
131             or die "Failed to log out";
132             $::logged_in = 0;
133             }
134              
135             =item _get_balances
136              
137             Gets account balance information (internal use only)
138              
139             =cut
140             sub _get_balances {
141             my $self = attr shift;
142              
143             my $result = $self->_login unless $::logged_in;
144              
145             # Change to use HTML::TreeBuilder
146             my ($user, undef, $private) = $result->content =~ m!
147            

welcome\s*([^<]+)

member\s*\#(\d+)\s*\(([^<]+)\)
148             !imox;
149             _decode_entities($user, +{ %entity2char, nbsp => ' ' });
150              
151             my $t = HTML::TreeBuilder->new;
152             my @accounts = map {
153             my ($name, $bal, $avail) = map { $_->as_text } $_->find('td');
154             ($name, my $account_no) = ($name =~ m/^([^(]+)\(([^)]+)\).*$/);
155             $avail =~ s/\$//; $bal =~ s/\$//; # get rid of currency sign
156             $avail =~ s/,//g; $bal =~ s/,//g; # Get rid of thousands separators
157             bless {
158             account_no => $account_no,
159             # Detect trailing parenthesis (negative number)
160             available => ($avail =~ /([\d+.]+)\)/) ? -$1 : $avail,
161             balance => ($bal =~ /([\d+.]+)\)/) ? -$1 : $bal,
162             name => $name,
163             parent => $self,
164             sort_code => $name,
165             transactions => [],
166             }, "Finance::Bank::NetBranch::Account";
167             } do {
168             $t->parse($result->content);
169             $t->eof;
170              
171             $t->look_down(
172             _tag => 'table',
173             sub {
174             my $table = $_[0];
175             $table->look_down(
176             _tag => 'th',
177             class => 'ColumnTitle',
178             sub { grep { /Balance/i } $_[0]->content_list },
179             sub { $_[0]->depth - $table->depth < 3 },
180             )
181             },
182             )->look_down(
183             _tag => 'tr',
184             sub { !$_[0]->find('th') },
185             sub { !grep { $_->as_text =~ /Total/i } $_[0]->content_list },
186             )
187             };
188             $t->delete;
189             $self->_logout;
190             $self->{accounts} = \@accounts
191             }
192              
193             =item _get_transactions
194              
195             Gets transaction information, given start and end dates (internal use only)
196              
197             =cut
198             sub _get_transactions {
199             my $self = attr shift;
200             my ($account, %args) = @_;
201              
202             $self->_login unless $::logged_in;
203             $::mech->follow_link(text_regex => qr/Account History/)
204             or die "Failed to open account history mech";
205              
206             my $result = $::mech->follow_link(
207             text_regex => qr/\($account->{account_no}\)/
208             ) or die "Failed to open history for account '$account->{account_no}'";
209              
210             # Convert dates into DateTime objects if necessary
211             my ($from, $to) = map {
212             ref($_) eq 'DateTime'
213             ? $_
214             : DateTime->from_epoch(epoch => $_)
215             } @args{qw(from to)};
216              
217             =item _pad0
218              
219             Pads a number to two digits with zeroes
220              
221             =cut
222              
223             sub _pad0 { sprintf "%0.2d", shift }
224              
225             $::mech->form_name('HistReq');
226              
227             $::mech->select('FM', _pad0($from->month));
228             $::mech->select('FD', $from->day);
229             $::mech->select('FY', $from->year);
230              
231             $::mech->select('TM', _pad0($to->month));
232             $::mech->select('TD', $to->day);
233             $::mech->select('TY', $to->year);
234              
235             $result = $::mech->submit
236             or die "Could not submit history request form";
237              
238             my $t = HTML::TreeBuilder->new;
239             # Reverse to put oldest transactions first
240             my @transactions = reverse map {
241             my ($date, $type, $desc, $amount, $bal) = map { $_->as_text } $_->find('td');
242             $date = DateTime->from_epoch(epoch => str2time($date));
243             $amount =~ s/\$//; $bal =~ s/\$//; # get rid of currency sign
244             $amount =~ s/,//g; $bal =~ s/,//g; # Get rid of thousands separators
245             $desc =~ s/\x{A0}/ /g;
246             bless {
247             # Detect trailing parenthesis (negative number)
248             amount => ($amount =~ /([\d+.]+)\)/) ? -$1 : $amount,
249             balance => ($bal =~ /([\d+.]+)\)/) ? -$1 : $bal,
250             date => $date,
251             description => $desc,
252             parent => $account,
253             type => $type,
254             }, "Finance::Bank::NetBranch::Transaction";
255             } do {
256             $t->parse($result->content);
257             $t->eof;
258              
259             $t->look_down(
260             _tag => 'table',
261             sub {
262             my $table = $_[0];
263             $table->look_down(
264             _tag => 'th',
265             class => 'ColumnTitle',
266             sub { grep { /New Balance/i } $_[0]->content_list },
267             sub { $_[0]->depth - $table->depth < 3 },
268             )
269             },
270             )->look_down(
271             _tag => 'tr',
272             sub { !$_[0]->find('th') },
273             )
274             };
275             $t->delete;
276             $self->_logout;
277             $self->{transactions} = \@transactions
278             }
279              
280             =back
281              
282             =head2 Finance::Bank::NetBranch::Account
283              
284             =over 4
285              
286             =item name
287              
288             =item sort_code
289              
290             =item account_no
291              
292             Return the account name, sort code or account number. The sort code is just the
293             name in this case, but it has been included for consistency with other
294             Finance::Bank::* modules.
295              
296             =item balance
297              
298             =item available
299              
300             Return the account balance or available amount as a signed floating point value.
301              
302             =cut
303              
304             package Finance::Bank::NetBranch::Account;
305             use Alias 'attr';
306             use Carp;
307              
308             no strict;
309              
310             =item AUTOLOAD
311              
312             Provides accessors (from Finance::Card::Citibank)
313              
314             =cut
315             sub AUTOLOAD { my $self = shift; $AUTOLOAD =~ s/.*:://; $self->{$AUTOLOAD} }
316              
317             =item transactions(from => $start_date, to => $end_date)
318              
319             Retrieves C objects for the specified
320             account object between two dates (unix timestamps or DateTime objects).
321              
322             =back
323              
324             =cut
325             sub transactions ($%) {
326             my $self = attr shift;
327             my (%args) = @_;
328             $args{from} && $args{to}
329             or croak "Must supply from and to dates";
330             @::transactions =
331             (@::transactions || @{ $::parent->_get_transactions($self, %args) });
332             }
333              
334             =head2 Finance::Bank::NetBranch::Transaction
335              
336             =over 4
337              
338             =item date
339              
340             =item type
341              
342             =item description
343              
344             =item amount
345              
346             =item balance
347              
348             Return appropriate data from this transaction.
349              
350             =cut
351             package Finance::Bank::NetBranch::Transaction;
352             no strict;
353              
354             =item AUTOLOAD
355              
356             Provides accessors (from Finance::Card::Citibank)
357              
358             =back
359              
360             =cut
361             sub AUTOLOAD { my $self = shift; $AUTOLOAD =~ s/.*:://; $self->{$AUTOLOAD} }
362              
363             1;
364              
365             __END__