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__ |