package Finance::Bank::LloydsTSB::Account; =head1 NAME Finance::Bank::LloydsTSB::Account - =head1 SYNOPSIS synopsis =head1 DESCRIPTION description =cut use strict; use warnings; our $DEBUG = 0; use Time::Local; use Finance::Bank::LloydsTSB::Statement; use Finance::Bank::LloydsTSB::utils qw(debug); sub ua { shift->{ua} } sub name { shift->{name} } sub descr_num { shift->{descr_num} } sub sort_code { shift->{sort_code} } sub account_no { shift->{account_no} } sub balance { shift->{balance} } sub parent { shift->{parent} } sub form_index { shift->{form_index} } sub _on_account_overview_page { my $self = shift; $self->debug("On account overview page?\n"); # Notice case difference of 'O' between this and navigation link to # this page if ($self->ua->content =~ /Account Overview/) { $self->debug("yes\n"); return 1; } else { $self->debug("no\n"); return 0; } } sub _navigate_to_account_overview { my $self = shift; if (! $self->_on_account_overview_page) { # This is the one we want # my @links = $self->ua->find_all_links; # my $correct = $links[0]->text; # GRR!   gets decoded into \xa0 by HTML::TokeParser # (which uses HTML::Entities::decode_entities) unless ($self->ua->follow_link(text_regex => qr/Account[ \xa0]overview/)) { # my $dumpfile = '/tmp/dump.html'; # $self->debug("Writing content to $dumpfile\n"); # $self->ua->save_content($dumpfile); # my @context = $self->ua->content =~ /(.{100}overview.{100,}?$)/gims; # die "no overview???", $self->ua->content # unless @context; # map "---\n$_\n---\n", @context; die "Couldn't go to account overview page\n"; } $self->debug("Gone to account overview page\n"); } } sub _on_account_details_page { my $self = shift; $self->debug("On details page for ", $self->name, "?\n"); if ($self->ua->content !~ /Your account details/) { $self->debug("no\n"); return 0; } if ($self->_check_selected_account_text) { $self->debug("yes\n"); return 1; } $self->debug("no\n"); return 0; } sub _check_selected_account_text { my $self = shift; # Look for a table which has the right 'Selected account' in the # header. my $te = new HTML::TableExtract; $te->parse($self->ua->content); foreach my $ts ($te->table_states) { my @rows = $ts->rows; my $header = $rows[0]; foreach my $td (@$header) { if ($td =~ /Selected account/ && (index($td, $self->{name}) >= 0)) { return 1; } } } return 0; } sub _navigate_to_details { my $self = shift; if ($self->_on_account_details_page) { $self->debug("Already on details page for ", $self->name, "\n"); return; } if (! $self->_on_account_overview_page) { $self->_navigate_to_account_overview; } $self->ua->follow_link(text => $self->name) or die "Couldn't go to account ", $self->name; $self->debug("Gone to ", $self->name, " details page\n"); } sub _on_account_statement_page { my $self = shift; $self->debug("On account statement page?\n"); if ($self->ua->content !~ /Your account statement/) { $self->debug("no\n"); return 0; } if ($self->_check_selected_account_text) { $self->debug("yes\n"); return 1; } $self->debug("yes\n"); return 1; } sub _navigate_to_statement { my $self = shift; if ($self->_on_account_statement_page) { $self->debug("Already on statement page for ", $self->name, "\n"); return; } $self->_navigate_to_account_overview; if (0) { # Go via details page $self->_navigate_to_details; $self->ua->follow_link(text => 'Statement') or die "Couldn't go to statement for ", $self->name; } else { # Go via Options form my $form = $self->ua->form_number($self->form_index); $self->ua->select('SelectAction', 'statement.ibc'); my $response = $self->ua->submit_form; die "submit of $self->name actions form failed with HTTP code ", $response->code, "\n" # also $self->ua->status unless $response->is_success; # also $self->ua->success } $self->debug("Gone to ", $self->name, " statement page\n"); } sub _on_account_statement_download_page { my $self = shift; $self->debug("On account statement download page?\n"); if ($self->ua->content !~ /Download a statement/) { $self->debug("no\n"); return 0; } if ($self->_check_selected_account_text) { $self->debug("yes\n"); return 1; } $self->debug("yes\n"); return 1; } sub _navigate_to_statement_download { my $self = shift; if ($self->_on_account_statement_download_page) { $self->debug("Already on statement download page for ", $self->name, "\n"); return; } $self->_navigate_to_statement; # $self->ua->select("selectbox", 6); # This used to work, but now the Go button has name="" :-( # $self->ua->click_button(value => "Go"); $self->ua->submit_form(form_name => "StatementSearch"); die "Go to download statement failed with HTTP ", $self->ua->status unless $self->ua->success; $self->debug("Gone to ", $self->name, " statement download page\n"); } =head2 fetch_statement Fetches a Finance::Bank::LloydsTSB::Statement object representing the latest statement page. =cut sub fetch_statement { my $self = shift; $self->_navigate_to_statement; my $te = new HTML::TableExtract( headers => [ "Date", "Payment Type", "Details", "Paid Out", "Paid In", "Balance", ] ); (my $html = $self->ua->content) =~ s/ / /g; $te->parse($html); my @tables = $te->tables; die "Couldn't find unique statement table" unless @tables == 1; my $DATE_RE = qr/^([ \d]\d)(\w\w\w)(\d\d)$/; my @transactions; my @fields = qw(date type details out in balance); ROW: foreach my $row ($tables[0]->rows) { my %transaction; for my $i (0 .. $#fields) { my $field = $fields[$i]; my $cell = $row->[$i]; if (ref $cell eq 'SCALAR') { $cell = $$cell; } elsif (ref($cell) =~ /HTML/) { $cell = $cell->as_trimmed_text; } next ROW unless defined $cell; $transaction{$field} = $cell; } next unless $transaction{date} =~ /\S/; if ($transaction{date} =~ $DATE_RE) { $transaction{dom} = $1; $transaction{month} = $2; $transaction{year} = $3; } else { warn "transaction date '", $transaction{date}, "' didn't match /$DATE_RE/\n"; } push @transactions, \%transaction; } return bless({ transactions => \@transactions, start_date => $transactions[ 0]{date}, end_date => $transactions[-1]{date}, }, 'Finance::Bank::LloydsTSB::Statement'); } =head2 download_statement($year, $month, $day, $duration) Downloads a statement in QIF format for time period starting on the given date, and returns it in a scalar. Duration options are taken from the website's HTML: e.g. 5 for 1 month's worth of QIF transactions. =cut sub download_statement { my $self = shift; my ($year, $month, $day, $duration) = @_; $self->debug("Downloading statement for $year/$month/$day, duration option $duration\n"); $self->_navigate_to_statement_download; $self->debug("\n"); $self->ua->set_fields( DateYear => $year, DateMonth => $month, DateDay => $day, DateRangeSelection => $duration, # radio button, 'DownloadLatest' is the other option 'Download' => 'DownloadDate', ); $self->ua->select('Format', 104); # download as QIF $self->ua->submit_form; $self->debug("Got content type ", $self->ua->ct, "\n"); my $qif = $self->ua->content; $self->ua->back; # so that we can still use logoff link or other links return $qif; } 1;