File Coverage

blib/lib/Finance/Bank/LloydsTSB/Account.pm
Criterion Covered Total %
statement 15 148 10.1
branch 0 48 0.0
condition 0 3 0.0
subroutine 5 24 20.8
pod 2 10 20.0
total 22 233 9.4


line stmt bran cond sub pod time code
1             package Finance::Bank::LloydsTSB::Account;
2              
3             =head1 NAME
4              
5             Finance::Bank::LloydsTSB::Account -
6              
7             =head1 SYNOPSIS
8              
9             synopsis
10              
11             =head1 DESCRIPTION
12              
13             description
14              
15             =cut
16              
17 1     1   4 use strict;
  1         2  
  1         26  
18 1     1   5 use warnings;
  1         1  
  1         42  
19              
20             our $VERSION = '1.35';
21             our $DEBUG = 0;
22              
23 1     1   5 use Time::Local;
  1         2  
  1         52  
24              
25 1     1   500 use Finance::Bank::LloydsTSB::Statement;
  1         2  
  1         34  
26 1     1   5 use Finance::Bank::LloydsTSB::utils qw(debug);
  1         2  
  1         1851  
27              
28 0     0 0   sub ua { shift->{ua} }
29 0     0 0   sub name { shift->{name} }
30 0     0 0   sub descr_num { shift->{descr_num} }
31 0     0 0   sub sort_code { shift->{sort_code} }
32 0     0 0   sub account_no { shift->{account_no} }
33 0     0 0   sub balance { shift->{balance} }
34 0     0 0   sub parent { shift->{parent} }
35 0     0 0   sub form_index { shift->{form_index} }
36              
37             sub _on_account_overview_page {
38 0     0     my $self = shift;
39 0           $self->debug("On account overview page?\n");
40              
41             # Notice case difference of 'O' between this and navigation link to
42             # this page
43 0 0         if ($self->ua->content =~ /Account Overview/) {
44 0           $self->debug("yes\n");
45 0           return 1;
46             }
47             else {
48 0           $self->debug("no\n");
49 0           return 0;
50             }
51             }
52              
53             sub _navigate_to_account_overview {
54 0     0     my $self = shift;
55              
56 0 0         if (! $self->_on_account_overview_page) {
57             # This is the one we want
58             # my @links = $self->ua->find_all_links;
59             # my $correct = $links[0]->text;
60              
61             # GRR!   gets decoded into \xa0 by HTML::TokeParser
62             # (which uses HTML::Entities::decode_entities)
63 0 0         unless ($self->ua->follow_link(text_regex =>
64             qr/Account[ \xa0]overview/))
65             {
66             # my $dumpfile = '/tmp/dump.html';
67             # $self->debug("Writing content to $dumpfile\n");
68             # $self->ua->save_content($dumpfile);
69             # my @context = $self->ua->content =~ /(.{100}overview.{100,}?$)/gims;
70             # die "no overview???", $self->ua->content
71             # unless @context;
72             # map "---\n$_\n---\n", @context;
73 0           die "Couldn't go to account overview page\n";
74             }
75 0           $self->debug("Gone to account overview page\n");
76             }
77             }
78              
79             sub _on_account_details_page {
80 0     0     my $self = shift;
81 0           $self->debug("On details page for ", $self->name, "?\n");
82              
83 0 0         if ($self->ua->content !~ /Your account details/) {
84 0           $self->debug("no\n");
85 0           return 0;
86             }
87              
88 0 0         if ($self->_check_selected_account_text) {
89 0           $self->debug("yes\n");
90 0           return 1;
91             }
92              
93 0           $self->debug("no\n");
94 0           return 0;
95             }
96              
97             sub _check_selected_account_text {
98 0     0     my $self = shift;
99              
100             # Look for a table which has the right 'Selected account' in the
101             # header.
102 0           my $te = new HTML::TableExtract;
103 0           $te->parse($self->ua->content);
104 0           foreach my $ts ($te->table_states) {
105 0           my @rows = $ts->rows;
106 0           my $header = $rows[0];
107 0           foreach my $td (@$header) {
108 0 0 0       if ($td =~ /Selected account/ && (index($td, $self->{name}) >= 0)) {
109 0           return 1;
110             }
111             }
112             }
113            
114 0           return 0;
115             }
116              
117             sub _navigate_to_details {
118 0     0     my $self = shift;
119 0 0         if ($self->_on_account_details_page) {
120 0           $self->debug("Already on details page for ", $self->name, "\n");
121 0           return;
122             }
123              
124 0 0         if (! $self->_on_account_overview_page) {
125 0           $self->_navigate_to_account_overview;
126             }
127              
128 0 0         $self->ua->follow_link(text => $self->name)
129             or die "Couldn't go to account ", $self->name;
130 0           $self->debug("Gone to ", $self->name, " details page\n");
131             }
132              
133             sub _on_account_statement_page {
134 0     0     my $self = shift;
135 0           $self->debug("On account statement page?\n");
136              
137 0 0         if ($self->ua->content !~ /Your account statement/) {
138 0           $self->debug("no\n");
139 0           return 0;
140             }
141              
142 0 0         if ($self->_check_selected_account_text) {
143 0           $self->debug("yes\n");
144 0           return 1;
145             }
146            
147 0           $self->debug("yes\n");
148 0           return 1;
149             }
150              
151             sub _navigate_to_statement {
152 0     0     my $self = shift;
153              
154 0 0         if ($self->_on_account_statement_page) {
155 0           $self->debug("Already on statement page for ", $self->name, "\n");
156 0           return;
157             }
158              
159 0           $self->_navigate_to_account_overview;
160              
161 0           if (0) {
162             # Go via details page
163             $self->_navigate_to_details;
164             $self->ua->follow_link(text => 'Statement')
165             or die "Couldn't go to statement for ", $self->name;
166             }
167             else {
168             # Go via Options form
169 0           my $form = $self->ua->form_number($self->form_index);
170 0           $self->ua->select('SelectAction', 'statement.ibc');
171 0           my $response = $self->ua->submit_form;
172 0 0         die "submit of $self->name actions form failed with HTTP code ",
173             $response->code, "\n" # also $self->ua->status
174             unless $response->is_success; # also $self->ua->success
175             }
176              
177 0           $self->debug("Gone to ", $self->name, " statement page\n");
178             }
179              
180             sub _on_account_statement_download_page {
181 0     0     my $self = shift;
182 0           $self->debug("On account statement download page?\n");
183              
184 0 0         if ($self->ua->content !~ /Download a statement/) {
185 0           $self->debug("no\n");
186 0           return 0;
187             }
188              
189 0 0         if ($self->_check_selected_account_text) {
190 0           $self->debug("yes\n");
191 0           return 1;
192             }
193            
194 0           $self->debug("yes\n");
195 0           return 1;
196             }
197              
198             sub _navigate_to_statement_download {
199 0     0     my $self = shift;
200              
201 0 0         if ($self->_on_account_statement_download_page) {
202 0           $self->debug("Already on statement download page for ", $self->name, "\n");
203 0           return;
204             }
205              
206 0           $self->_navigate_to_statement;
207              
208             #
209 0           $self->ua->select("selectbox", 6);
210              
211             # This used to work, but now the Go button has name="" :-(
212             # $self->ua->click_button(value => "Go");
213 0           $self->ua->submit_form(form_name => "StatementSearch");
214              
215 0 0         die "Go to download statement failed with HTTP ", $self->ua->status
216             unless $self->ua->success;
217 0           $self->debug("Gone to ", $self->name, " statement download page\n");
218             }
219              
220             =head2 fetch_statement
221              
222             Fetches a Finance::Bank::LloydsTSB::Statement object representing the
223             latest statement page.
224              
225             =cut
226              
227             sub fetch_statement {
228 0     0 1   my $self = shift;
229              
230 0           $self->_navigate_to_statement;
231              
232 0           my $te = new HTML::TableExtract(
233             headers => [
234             "Date",
235             "Payment Type",
236             "Details",
237             "Paid Out",
238             "Paid In",
239             "Balance",
240             ]
241             );
242 0           (my $html = $self->ua->content) =~ s/ / /g;
243 0           $te->parse($html);
244 0           my @tables = $te->tables;
245 0 0         die "Couldn't find unique statement table" unless @tables == 1;
246              
247 0           my $DATE_RE = qr/^([ \d]\d)(\w\w\w)(\d\d)$/;
248              
249 0           my @transactions;
250 0           my @fields = qw(date type details out in balance);
251              
252             ROW:
253 0           foreach my $row ($tables[0]->rows) {
254 0           my %transaction;
255 0           for my $i (0 .. $#fields) {
256 0           my $field = $fields[$i];
257 0           my $cell = $row->[$i];
258 0 0         if (ref $cell eq 'SCALAR') {
    0          
259 0           $cell = $$cell;
260             }
261             elsif (ref($cell) =~ /HTML/) {
262 0           $cell = $cell->as_trimmed_text;
263             }
264 0 0         next ROW unless defined $cell;
265 0           $transaction{$field} = $cell;
266             }
267 0 0         next unless $transaction{date} =~ /\S/;
268 0 0         if ($transaction{date} =~ $DATE_RE) {
269 0           $transaction{dom} = $1;
270 0           $transaction{month} = $2;
271 0           $transaction{year} = $3;
272             }
273             else {
274 0           warn "transaction date '", $transaction{date}, "' didn't match /$DATE_RE/\n";
275             }
276 0           push @transactions, \%transaction;
277             }
278              
279 0           return bless({
280             transactions => \@transactions,
281             start_date => $transactions[ 0]{date},
282             end_date => $transactions[-1]{date},
283             }, 'Finance::Bank::LloydsTSB::Statement');
284             }
285              
286             =head2 download_statement($year, $month, $day, $duration)
287              
288             Downloads a statement in QIF format for time period starting on the
289             given date, and returns it in a scalar.
290              
291             Duration options are taken from the website's HTML:
292              
293            
294            
295            
296            
297            
298            
299            
300              
301             e.g. 5 for 1 month's worth of QIF transactions.
302              
303             =cut
304              
305             sub download_statement {
306 0     0 1   my $self = shift;
307 0           my ($year, $month, $day, $duration) = @_;
308              
309 0           $self->debug("Downloading statement for $year/$month/$day, duration option $duration\n");
310 0           $self->_navigate_to_statement_download;
311 0           $self->debug("\n");
312              
313 0           $self->ua->set_fields(
314             DateYear => $year,
315             DateMonth => $month,
316             DateDay => $day,
317             DateRangeSelection => $duration,
318             # radio button, 'DownloadLatest' is the other option
319             'Download' => 'DownloadDate',
320             );
321              
322 0           $self->ua->select('Format', 104); # download as QIF
323 0           $self->ua->submit_form;
324 0           my $ok = 0;
325 0           my $ct = $self->ua->ct;
326 0 0         if ($ct eq 'text/x-qif') {
327 0           $self->debug("Got content type $ct\n");
328 0           $ok = 1;
329             }
330             else {
331 0           warn "Expected content type 'text/x-qif' but got '$ct'\n";
332             }
333 0           my $qif = $self->ua->content;
334              
335             # For some extremely weird reason, if we do this:
336              
337             # # pop .qif download off stack so that we can still use other links
338             # $self->debug("Going back a page\n");
339             # $self->ua->back;
340              
341             # then $self->ua->follow_link either leads us back to the login
342             # page or gets us into a state where we can't do anything.
343              
344             # A workaround is to start at the beginning:
345 0           $self->ua->get("https://online.lloydstsb.co.uk/customer.ibc");
346             # although this time we don't need to log in.
347              
348 0           return ($ct, $qif);
349             }
350              
351             1;
352