Finance::Bank::US::INGDirect - Check balances and transactions for US INGDirect accounts


Finance-Bank-US-INGDirect documentation Contained in the Finance-Bank-US-INGDirect distribution.

Index


Code Index:

NAME

Top

Finance::Bank::US::INGDirect - Check balances and transactions for US INGDirect accounts

VERSION

Top

Version 0.05

SYNOPSIS

Top

  use Finance::Bank::US::INGDirect;
  use Finance::OFX::Parse::Simple;

  my $ing = Finance::Bank::US::INGDirect->new(
      saver_id => '...',
      customer => '########',
      questions => {
          # Your questions may differ; examine the form to find them
          'AnswerQ1.4' => '...', # In what year was your mother born?
          'AnswerQ1.5' => '...', # In what year was your father born?
          'AnswerQ1.8' => '...', # What is the name of your hometown newspaper?
      },
      pin => '########',
  );

  my $parser = Finance::OFX::Parse::Simple->new;
  my @txs = @{$parser->parse_scalar($ing->recent_transactions)};
  my %accounts = $ing->accounts;

  for (@txs) {
      print "Account: $_->{account_id}\n";
      printf "%s %-50s %8.2f\n", $_->{date}, $_->{name}, $_->{amount} for @{$_->{transactions}};
      print "\n";
  }

DESCRIPTION

Top

This module provides methods to access data from US INGdirect accounts, including account balances and recent transactions in OFX format (see Finance::OFX and related modules). It also provides a method to transfer money from one account to another on a given date.

METHODS

Top

new( saver_id => '...', customer => '...', questions => {...}, pin => '...' )

Return an object that can be used to retrieve account balances and statements. See SYNOPSIS for examples of challenge questions.

accounts( )

Retrieve a list of accounts:

  ( '####' => [ number => '####', type => 'Orange Savings', nickname => '...',
                available => ###.##, balance => ###.## ],
    ...
  )

recent_transactions( $account, $days )

Retrieve a list of transactions in OFX format for the given account (default: all accounts) for the past number of days (default: 30).

transactions( $account, $from, $to )

Retrieve a list of transactions in OFX format for the given account (default: all accounts) in the given time frame (default: pretty far in the past to pretty far in the future).

transfer( $from, $to, $amount, $when )

Transfer money from one account number to another on the given date (default: immediately). Returns the confirmation number. Use at your own risk.

AUTHOR

Top

This version by Steven N. Severinghaus <sns-perl@severinghaus.org>

COPYRIGHT

Top

SEE ALSO

Top

Finance::Bank::INGDirect, Finance::OFX::Parse::Simple


Finance-Bank-US-INGDirect documentation Contained in the Finance-Bank-US-INGDirect distribution.
package Finance::Bank::US::INGDirect;

use strict;

use Carp 'croak';
use LWP::UserAgent;
use HTTP::Cookies;
use HTML::Strip;
use Date::Parse;
use Data::Dumper;

our $VERSION = '0.05';

my $base = 'https://secure.ingdirect.com/myaccount';

sub new {
    my ($class, %opts) = @_;
    my $self = bless \%opts, $class;

    $self->{ua} ||= LWP::UserAgent->new(cookie_jar => HTTP::Cookies->new);

    _login($self);
    $self;
}

sub _login {
    my ($self) = @_;

    my $response = $self->{ua}->get("$base/INGDirect/login.vm");

    $response = $self->{ua}->post("$base/INGDirect/login.vm", [
        publicUserId => $self->{saver_id},
    ]);
    $response->is_redirect or croak "Initial login failed.";

    $response = $self->{ua}->get("$base/INGDirect/security_questions.vm");
    $response->is_success or croak "Retrieving challenge questions failed.";

    my @questions = map { s/^.*(AnswerQ.*)span".*$/$1/; $_ }
        grep /AnswerQ/,
        split('\n', $response->content);
    croak "Didn't understand questions." if @questions != 2;

    $response = $self->{ua}->post("$base/INGDirect/security_questions.vm", [
        TLSearchNum => $self->{customer},
        'customerAuthenticationResponse.questionAnswer[0].answerText' => $self->{questions}{$questions[0]},
        'customerAuthenticationResponse.questionAnswer[1].answerText' => $self->{questions}{$questions[1]},
        '_customerAuthenticationResponse.device[0].bind' => 'false',
    ]);
    $response->is_redirect or croak "Submitting challenge responses failed.";

    $response = $self->{ua}->get("$base/INGDirect/login_pinpad.vm");
    $response->is_success or croak "Loading PIN form failed.";

    my @keypad = map { s/^.*mouseUpKb\("([A-Z])".*$/$1/; $_ }
        grep /pinKeyboard[A-Z]number/,
        split('\n', $response->content);

    unshift(@keypad, pop @keypad);

    $response = $self->{ua}->post("$base/INGDirect/login_pinpad.vm", [
        'customerAuthenticationResponse.PIN' => join '', map { $keypad[$_] } split//, $self->{pin},
    ]);
    $response->is_redirect or croak "Submitting PIN failed.";

    $response = $self->{ua}->get("$base/INGDirect.html?command=viewAccountPostLogin");
    $response->is_success or croak "Final login failed.";
    $self->{_account_screen} = $response->content;
}

sub accounts {
    my ($self) = @_;

    my $hs = HTML::Strip->new;
    my @lines = grep /command=goToAccount/, split(/[\n\r]/, $self->{_account_screen});
    @lines = map { tr/\xa0/ /; $_ } split(/\n/, $hs->parse(join "\n", @lines));

    my %accounts;
    for (@lines) {
        my @data = splice(@lines, 0, 3);
        my %account;
        ($account{type} = $data[0]) =~ s/^\s*(.*?)\s*$/$1/;
        ($account{nickname}, $account{number}, $account{balance}) = split /\s/, $data[1];
        ($account{available} = $data[2]) =~ s/^\s*(.*?)\s*$/$1/;
        $accounts{$account{number}} = \%account;
    }

    %accounts;
}

sub recent_transactions {
    my ($self, $account, $days) = @_;

    $account ||= 'ALL';
    $days ||= 30;

    my $response = $self->{ua}->post("$base/download.qfx", [
        type => 'OFX',
        TIMEFRAME => 'STANDARD',
        account => $account,
        FREQ => $days,
    ]);
    $response->is_success or croak "OFX download failed.";

    $response->content;
}

sub transactions {
    my ($self, $account, $from, $to) = @_;

    $account ||= 'ALL';
    $from ||= '2000-01-01';
    $to ||= '2038-01-01';

    my @from = strptime($from);
    my @to = strptime($to);

    $from[4]++;
    $to[4]++;
    $from[5] += 1900;
    $to[5] += 1900;

    my $response = $self->{ua}->post("$base/download.qfx", [
        type => 'OFX',
        TIMEFRAME => 'VARIABLE',
        account => $account,
        startDate => sprintf("%02d/%02d/%d", @from[4,3,5]),
        endDate   => sprintf("%02d/%02d/%d", @to[4,3,5]),
    ]);
    $response->is_success or croak "OFX download failed.";

    $response->content;
}

sub transfer {
    my ($self, $from, $to, $amount, $when) = @_;
    my $type = $when ? 'SCHEDULED' : 'NOW';

    if($when) {
        my @when = strptime($when);
        $when[4]++;
        $when[5] += 1900;
        $when = sprintf("%02d/%02d/%d", @when[4,3,5]);
    }

    my $response = $self->{ua}->get("$base/INGDirect/money_transfer.vm");
    my ($page_token) = map { s/^.*value="(.*?)".*$/$1/; $_ }
        grep /<input.*name="pageToken"/,
        split('\n', $response->content);

    $response = $self->{ua}->post("$base/INGDirect/deposit_transfer_input.vm", [
        pageToken => $page_token,
        action => 'continue',
        amount => $amount,
        sourceAccountNumber => $from,
        destinationAccountNumber => $to,
        depositTransferType => $type,
        $when ? (scheduleDate => $when) : (),
    ]);
    $response->is_redirect or croak "Transfer setup failed.";

    $response = $self->{ua}->get("$base/INGDirect/deposit_transfer_validate.vm");
    ($page_token) = map { s/^.*value="(.*?)".*$/$1/; $_ }
        grep /<input.*name="pageToken"/,
        split('\n', $response->content);

    $response = $self->{ua}->post("$base/INGDirect/deposit_transfer_validate.vm", [
        pageToken => $page_token,
        action => 'submit',
    ]);
    $response->is_redirect or croak "Transfer validation failed. Check your account!";

    $response = $self->{ua}->get("$base/INGDirect/deposit_transfer_confirmation.vm");
    $response->is_success or croak "Transfer confirmation failed. Check your account!";
    my ($confirmation) = map { s/^.*Number">(\d+)<.*$/$1/; $_ }
        grep /<span.*id="confirmationNumber">/,
        split('\n', $response->content);

    $confirmation;
}

1;