# Filename: OFX.pm
# Serialize, Parse and Query using the Open Financial Exchange format
# http://www.ofx.net/
# 
# Created January 30, 2008	Brandon Fosdick <bfoz@bfoz.net>
#
# Copyright 2008 Brandon Fosdick <bfoz@bfoz.net> (BSD License)
#
# $Id: OFX.pm,v 1.2 2008/03/04 04:22:24 bfoz Exp $

package Finance::OFX;

use strict;
use warnings;

our $VERSION = '2';

use Finance::OFX::UserAgent;

use HTTP::Date;

sub new
{
    my ($this, %options) = @_;
    my $class = ref($this) || $this;
    my $self = {};
    bless $self, $class;

    # Assume any unprocessed options are meant for the UserAgent, 
    #  which implies that this object will be used to generate and process 
    #  requests instead of processing stored files
    $self->{ua} = Finance::OFX::UserAgent->new(%options) if scalar keys %options;

    return $self;
}

# --- Getters and Setters ---

sub institution
{
    my $s = shift;
    $s->{ua}->institution(@_);
}

sub response
{
    my $s = shift;
    $s->{ua}->{response};
}

sub user_id
{
    my $s = shift;
    $s->{ua}->user_id(@_);
}

sub user_pass
{
    my $s = shift;
    $s->{ua}->user_pass(@_);
}

# --- Public Methods ---

sub accounts
{
    my $s = shift;

    my $r = $s->{ua}->account_info;

    return -1 unless $r->is_success;
    return -2 if $r->signon_status_code;
    return undef unless $r->ofx->{signupmsgsrsv1}{acctinfotrnrs}{acctinfors}{acctinfo};

    my @accounts;
    for( @{$r->ofx->{signupmsgsrsv1}{acctinfotrnrs}{acctinfors}{acctinfo}} )
    {
	my %b = flatten($_);
	push @accounts, \%b;
    }
    return @accounts;
}

sub balance
{
    my ($s, $acct) = @_;
    return undef unless ref($acct) eq 'Finance::OFX::Account';

    # Use the FID from the given FI if none is set in the given Account
    $acct->fid($s->institution()->fid()) unless $acct->fid();

    my $r = $s->{ua}->statement($acct);
    return -1 unless $r->is_success;
    return -2 if $r->signon_status_code;
    return undef unless $r->ofx->{bankmsgsrsv1}{stmttrnrs};

    my $transaction = $r->ofx->{bankmsgsrsv1}{stmttrnrs};
    return "Statement request error: ".($transaction->{status}{message}) if transactionStatusCode($transaction);
    return "No statement info returned" unless exists $transaction->{stmtrs};

    return $transaction->{stmtrs};
}

sub transactions
{
    my ($s, $acct, $start, $end) = @_;
    return undef unless ref($acct) eq 'Finance::OFX::Account';

    # Use the FID from the given FI if none is set in the given Account
    $acct->fid($s->institution()->fid()) unless $acct->fid();

    my %options;
    $options{transactions} = 'Y';
    $options{start} = $start if $start;
    $options{end} = $end if $end;

    my $r = $s->{ua}->statement($acct, %options);
    return -1 unless $r->is_success;
    return -2 if $r->signon_status_code;
    my $ofx = $r->ofx;
    return undef unless $ofx->{bankmsgsrsv1}{stmttrnrs}{stmtrs};
    my $transaction = $ofx->{bankmsgsrsv1}{stmttrnrs};
    return "Statement request error: ".($transaction->{status}{message}) if transactionStatusCode($transaction);

    return $transaction->{stmtrs}{banktranlist};
}

sub transactionStatusCode
{
    my $tree = shift;
    return undef unless ref($tree) eq 'HASH';
    return $tree->{status}{code};
}

# --- Internal use only ---

# Blindly flatten a HoH
sub flatten
{
    my $tree = shift;
    return $tree unless ref($tree) eq 'HASH';
    my %a;
    for( keys %{$tree} )
    {
	$a{$_} = $tree->{$_}, next unless ref($tree->{$_}) eq 'HASH';
	my %b = flatten($tree->{$_});
	@a{keys %b} = values %b;
    }
    return %a;
}

1;

__END__

=head1 NAME

Finance::OFX - An OFX client implementation.

=head1 SYNOPSIS

 use Finance::OFX;
 use Finance::OFX::Institution;
 
 my $fi = Finance::OFX::Institution->new(ORG => $org, FID => $fid, URL => $url);
 my $ofx = Finance::OFX->new(userID=>$user, userPass=>$pass, Institution => $fi);
 my @accounts = $ofx->accounts;

=head1 DESCRIPTION

C<Finance::OFX> provides several convenience functions for interacting with OFX servers.

=head1 CONSTRUCTOR

=over

=item $ua = Finance::OFX->new( %options )

Constructs a new C<Finance::OFX> object and returns it. C<%options> can be 
anything accepted by L<Finance::OFX::UserAgent>.

=back

=head1 ATTRIBUTES

=over

=item $ofx->institution

Get/Set the L<Finance::OFX::Institution> object used by the 
L<Finance::OFX::UserAgent> object.

=item $ofx->response

Get the most recent L<Finance::OFX::Response> generated by 
L<Finance::OFX::UserAgent>.

=item $ofx->user_id

Get/Set the OFX user ID. Wraps L<Finance::OFX::UserAgent::user_id()>.

=item $ofx->user_pass

Get/Set the OFX user password. Wraps L<Finance::OFX::UserAgent::user_pass()>.

=back

=head1 METHODS

These are convenience functions that wrap calls to L<Finance::OFX::UserAgent> 
and post-process the results.

=over

=item $ofx->accounts()

Get a list of the user's accounts at the configured Financial Institution.

=item $ofx->balance( $acct )

Get the latest balance statement for the given account at the configured 
Financial Institution.

=item $ofx->transactions( $acct [ $start [ $end ] ] )

Get the transaction list for the given account. Some Financial Institutions 
will accept a full, or partial, date range. Others simply ignore the date range.

=back

=head1 SEE ALSO

L<Finance::OFX::Account>
L<Finance::OFX::Institution>
L<Finance::OFX::Parse>
L<Finance::OFX::UserAgent>
L<http://ofx.net>

=head1 WARNING

From C<Finance::Bank::LloydsTSB>:

This is code for B<online banking>, and that means B<your money>, and
that means B<BE CAREFUL>. You are encouraged, nay, expected, to audit
the source of this module yourself to reassure yourself that I am not
doing anything untoward with your banking data. This software is useful
to me, but is provided under B<NO GUARANTEE>, explicit or implied.

=head1 AUTHOR

Brandon Fosdick, E<lt>bfoz@bfoz.netE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright 2008 Brandon Fosdick <bfoz@bfoz.net>

This software is provided under the terms of the BSD License.

=cut
