<commit_before>package Finance::Bank::LloydsTSB;

=head1 NAME

Finance::Bank::LloydsTSB - Check your bank accounts from Perl

=head1 SYNOPSIS

  use Finance::Bank::LloydsTSB;
  my @accounts = Finance::Bank::LloydsTSB->check_balance(
        username  => $username,
        password  => <PASSWORD>
        memorable => $memorable_phrase
  );
  for my $acc (@accounts) {
    printf "%20s : %8s / %19s : GBP %9.2f\n",
      $acc->name, ($acc->sort_code || ''), $acc->account_no, $acc->balance;
  }
  my $statement = $accounts[0]->fetch_statement;

  # Retrieve QIF for all transactions in January 2008.
  my $qif = $accounts[1]->download_statement(2008, 01, 01, 5);

=head1 DESCRIPTION

This module provides a rudimentary interface to the LloydsTSB online
banking system at C<https://online.lloydstsb.co.uk/>. You will need
either C<Crypt::SSLeay> or C<IO::Socket::SSL> installed for HTTPS
support to work with LWP.

=cut

use strict;
use warnings;

use Carp;

our $VERSION = '1.34';
our $DEBUG = 0;

use Carp qw(carp cluck croak confess);
use HTML::TableExtract qw(tree);
use WWW::Mechanize;

use Finance::Bank::LloydsTSB::utils qw(debug trim);
use Finance::Bank::LloydsTSB::Account;

our $ua = WWW::Mechanize->new(
    env_proxy => 1, 
    keep_alive => 1, 
    timeout => 30,
); 

=head1 CLASS METHODS

=cut

sub _login {
    my $self = shift;

    $ua->get("https://online.lloydstsb.co.uk/customer.ibc");
    my $field = $ua->current_form->find_input("UserId1");
    $field->{type}="input";
    bless $field, "HTML::Form::TextInput";
    $ua->field(UserId1  => $self->{username});
    $ua->field(Password => $self->{password});
    $ua->click;

    # Now we're at the new "memorable information" page, so parse that
    # and input the right form data.

    for (0..2) {
        my $key;
        eval { $key = $ua->current_form->find_input("ResponseKey$_")->value; };
        croak "Couldn't log in; check your password and username" if $@;
        my $value = substr(lc $self->{memorable}, $key-1, 1);
        $ua->field("ResponseValue$_" => $value);
    }

    $ua->click;
}

=head2 get_accounts(username => $u, password => $p, memorable => $m)

Return a list of Finance::Bank::LloydsTSB::Account objects, one for
each of your bank accounts.

=cut

sub get_accounts {
    my ($class, %opts) = @_;
    croak "Must provide a password" unless exists $opts{password};
    croak "Must provide a username" unless exists $opts{username};
    croak "Must provide memorable information" unless exists $opts{memorable};

    my $self = bless { %opts }, $class;

    $self->_login;

    if ($ua->content =~ /To suppress a message/i) {
        warn "Got messages screen; clicking through ...\n";
        $ua->click;
    }

    croak "Failed memorable info stage:", $ua->content
      unless $ua->content =~ /Account\s+list/i;

    # Now we have the account list page; we need to parse it.
    my $te = new HTML::TableExtract(
        headers => [
            "Account name",
            "Balance",
            "O/D Limit",
            "Options",
        ],
        # Only use keep_html if extraction mode is raw text/HTML
        # i.e. subclass of HTML::Parser!  Otherwise there seems to be
        # a bug which includes start tag in the text segment.
        # keep_html => 1,
    );
    my $html = $ua->content;
    $html =~ s/&nbsp;?/ /g;
    $te->parse($html);
    my @tables = $te->tables;
    croak "HTML::TableExtract failed to find table:\n$html" unless @tables;
    croak "HTML::TableExtract found >1 tables" unless @tables == 1;

    my $acc_action_forms = $class->_get_acc_action_form_mapping;

    # Assume only one matching table using $te->rows shorthand
    my @accounts;
    foreach my $row ($te->rows) {<commit_msg>"Account list" has now changed to "Account Overview"<commit_after>package Finance::Bank::LloydsTSB;

=head1 NAME

Finance::Bank::LloydsTSB - Check your bank accounts from Perl

=head1 SYNOPSIS

  use Finance::Bank::LloydsTSB;
  my @accounts = Finance::Bank::LloydsTSB->check_balance(
        username  => $username,
        password  => <PASSWORD>
        memorable => $memorable_phrase
  );
  for my $acc (@accounts) {
    printf "%20s : %8s / %19s : GBP %9.2f\n",
      $acc->name, ($acc->sort_code || ''), $acc->account_no, $acc->balance;
  }
  my $statement = $accounts[0]->fetch_statement;

  # Retrieve QIF for all transactions in January 2008.
  my $qif = $accounts[1]->download_statement(2008, 01, 01, 5);

=head1 DESCRIPTION

This module provides a rudimentary interface to the LloydsTSB online
banking system at C<https://online.lloydstsb.co.uk/>. You will need
either C<Crypt::SSLeay> or C<IO::Socket::SSL> installed for HTTPS
support to work with LWP.

=cut

use strict;
use warnings;

use Carp;

our $VERSION = '1.34';
our $DEBUG = 0;

use Carp qw(carp cluck croak confess);
use HTML::TableExtract qw(tree);
use WWW::Mechanize;

use Finance::Bank::LloydsTSB::utils qw(debug trim);
use Finance::Bank::LloydsTSB::Account;

our $ua = WWW::Mechanize->new(
    env_proxy => 1, 
    keep_alive => 1, 
    timeout => 30,
); 

=head1 CLASS METHODS

=cut

sub _login {
    my $self = shift;

    $ua->get("https://online.lloydstsb.co.uk/customer.ibc");
    my $field = $ua->current_form->find_input("UserId1");
    $field->{type}="input";
    bless $field, "HTML::Form::TextInput";
    $ua->field(UserId1  => $self->{username});
    $ua->field(Password => $self->{password});
    $ua->click;

    # Now we're at the new "memorable information" page, so parse that
    # and input the right form data.

    for (0..2) {
        my $key;
        eval { $key = $ua->current_form->find_input("ResponseKey$_")->value; };
        croak "Couldn't log in; check your password and username" if $@;
        my $value = substr(lc $self->{memorable}, $key-1, 1);
        $ua->field("ResponseValue$_" => $value);
    }

    $ua->click;
}

=head2 get_accounts(username => $u, password => $p, memorable => $m)

Return a list of Finance::Bank::LloydsTSB::Account objects, one for
each of your bank accounts.

=cut

sub get_accounts {
    my ($class, %opts) = @_;
    croak "Must provide a password" unless exists $opts{password};
    croak "Must provide a username" unless exists $opts{username};
    croak "Must provide memorable information" unless exists $opts{memorable};

    my $self = bless { %opts }, $class;

    $self->_login;

    if ($ua->content =~ /To suppress a message/i) {
        warn "Got messages screen; clicking through ...\n";
        $ua->click;
    }

    croak "Couldn't find account overview at memorable info stage:", $ua->content
      unless $ua->content =~ /Account\s+Overview/;

    # Now we have the account list page; we need to parse it.
    my $te = new HTML::TableExtract(
        headers => [
            "Account name",
            "Balance",
            "O/D Limit",
            "Options",
        ],
        # Only use keep_html if extraction mode is raw text/HTML
        # i.e. subclass of HTML::Parser!  Otherwise there seems to be
        # a bug which includes start tag in the text segment.
        # keep_html => 1,
    );
    my $html = $ua->content;
    $html =~ s/&nbsp;?/ /g;
    $te->parse($html);
    my @tables = $te->tables;
    croak "HTML::TableExtract failed to find table:\n$html" unless @tables;
    croak "HTML::TableExtract found >1 tables" unless @tables == 1;

    my $acc_action_forms = $class->_get_acc_action_form_mapping;

    # Assume only one matching table using $te->rows shorthand
    my @accounts;
    foreach my $row ($te->rows) {
{'fraction_non_alphanumeric': 0.14642904641574678, 'fraction_numerical': 0.011703684000531985, 'mean_word_length': 3.0958605664488017, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 109, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3840826', 'n_tokens_mistral': 2595, 'n_tokens_neox': 2489, 'n_words': 741}