#!/usr/bin/perl
#
# get_discover_ofx.pl
#
# A quick hack to download the OFX formated data describing your Discover Card
# transactions between any two dates.  For usage instructions, try
# the '-help' flag.
#
# If you intend to use the downloaded OFX without importing it into some
# financial software package, you might find the (equally quick) hack by the
# name of PrettyPrintXML.pl to be useful.  It should be able to be found where
# you got this script.  It will make it *much* easier to parse visually...
#
# e.g.
# $ ./get_discover_ofx.pl 6011222233334444 Y N | ./PrettyPrintXML.pl
#
# Copyright (C) 2004 Craig B. Agricola
#
# get_discover_ofx.pl is free software; you can redistribute it and/or
# modify it under the terms of "The Artistic License", which can be found
# at http://www.opensource.org/licenses/artistic-license.php
#
# $Id: get_discover_ofx.pl,v 1.2 2004/04/15 21:37:44 agricolc Exp $

use LWP;
use Time::Local;

sub fmtdate {
  my ($time) = @_;
  my (@t) = reverse((localtime($time))[0..5]);
  $t[0]+=1900;
  $t[1]+=1;
  return(sprintf("%04d".("%02d"x5), @t));
}

sub gen_transaction_id {
  my $trnuid;
  for(my $i=0; $i<16; $i++) { $trnuid .= chr(int(rand(26))+65) }
  return($trnuid);
}

sub get_password {
  my $password;
  system("stty", "-echo");
  my $tty = `tty`;
  chop($tty);
  open(TTY, "+<$tty");
  print TTY ("Password: ");
  chop($password = <TTY>);
  print TTY "\n";
  close(TTY);
  system("stty", "echo");
  return($password);
}

sub parse_date {
  my ($argv) = @_;
  my $date = shift(@{$argv});
  if (!defined($date)) {
    $date = undef;
  } elsif ($date =~ /^([NDWMY])?(([+-]\d+[smhDWMY])*)$/) {
    my $ds = $2;
    if (($1 eq "N") || ($1 eq "")) {
      $date = time();
    } elsif ($1 eq "D") {
      $date = timelocal(0,0,0,(localtime(time()))[3,4,5]);
    } elsif ($1 eq "W") {
      my (@now) = (localtime(time()))[3,4,5,6];
      $date = timelocal(0,0,0,@now[0,1,2]);
      $date -= $now[3] * 24 * 60 * 60;
    } elsif ($1 eq "M") {
      $date = timelocal(0,0,0,1,(localtime(time()))[4,5]);
    } elsif ($1 eq "Y") {
      $date = timelocal(0,0,0,1,0,(localtime(time()))[5]);
    }
    while ($ds=~/^([+-])(\d+)([smhDWMY])(.*)$/) {
      $ds = $4;
      my $inc = $2;
      if ($3 eq "M") {
        my $sign = ($1 eq "-")?-1:1;
        my @date = localtime($date);
        $date[5] += $sign * int($inc/12);
        $date[4] += $sign * ($inc % 12);
        if ($date[4]<0) { $date[5]--; $date[4]+=12; }
        $date = timelocal(@date);
      } else {
        if    ($1 eq "-") { $inc *= -1; }
        if    ($3 eq "m") { $inc *= 60; }
        elsif ($3 eq "h") { $inc *= 60 * 60; }
        elsif ($3 eq "D") { $inc *= 60 * 60 * 24; }
        elsif ($3 eq "W") { $inc *= 60 * 60 * 24 * 7; }
        elsif ($3 eq "Y") { $inc *= 60 * 60 * 24 * 365; }
        $date += $inc;
      }
    }
  } elsif ($date =~ m#^(\d+)/(\d+)/(\d+)(_(\d+):(\d+)(:(\d+))?)?#) {
    $date = timelocal($8,$6,$5,$3,$2-1,$1-1900);
  }
  return($date);
}

sub make_ofx_request {
  my ($userid, $userpass, $startdate, $enddate) = @_;
  my $req_content = join("",<DATA>);
  my $curdate = fmtdate(time());
  my $trnuid = gen_transaction_id();
  if (defined($startdate)) {
    $startdate = fmtdate($startdate);
    $enddate = fmtdate($enddate);
    $req_content =~ s#(<DTSTART>).*(</DTSTART>)#$1$startdate$2#;
    $req_content =~ s#(<DTEND>).*(</DTEND>)#$1$enddate$2#;
  } else {
    $req_content =~ s#(<DTSTART>).*(</DTSTART>)##;
    $req_content =~ s#(<DTEND>).*(</DTEND>)##;
  }
  $req_content =~ s#(<DTCLIENT>).*(</DTCLIENT>)#$1$curdate$2#;
  $req_content =~ s#(<TRNUID>).*(</TRNUID>)#$1$trnuid$2#;
  $req_content =~ s#(<USERID>).*(</USERID>)#$1$userid$2#;
  $req_content =~ s#(<ACCTID>).*(</ACCTID>)#$1$userid$2#;
  $req_content =~ s#(<USERPASS>).*(</USERPASS>)#$1$userpass$2#;
  return($req_content);
}

##################################### MAIN ####################################

if (grep(/^-h/, @ARGV)) {
  print <<'EOF';

Arguments: <account> <start_date> <end_date>

           <account>    is your 16 digit Discover Card number
           <start_date> specifies the date after which you want transactions
           <end_date>   specifies the date before which you want transactions

           If the <start_date> and <end_date> are not supplied, all
           transactions available on the server will be downloaded.  If
           <start_date> is supplied, but <end_date> is not, then the current
           time will be used for <end_date>.

           You will be prompted for your password, which is the password you
           use for logging into the Account Center at http://discovercard.com/

           <start_date> and <end_date> are encoded as either:
             YYYY/MM/DD_hh:mm:ss
           OR
             First character specifies the base time:
               N - [N]ow        (current time)
               D - to[D]ay      (12:00AM this morning)
               W - this [W]eek  (12:00AM of this preceeding Sunday)
               M - this [M]onth (12:00AM of the first day of this month)
               Y - this [Y]ear  (12:00AM of the first day of this year)
             After that, additions or subtractions to the base time:
               [+-]nnn[smhDWMY]
               s - [s]econds
               m - [m]inutes
               h - [h]ours
               D - [D]ays
               W - [W]eeks
               M - [M]onths
               Y - [Y]ears
             If the base time isn't specified, an 'N' (now) is implicit

             Examples:
               N-3D+1h            - Three days ago from an hour from now
               D-1M+7h+30m        - 7:30AM one month ago
               W+3D               - The Wednesday of this week
               M-5M+17d           - The 17th day of 5 months ago
               Y+4M+5D+16h+45m    - 4:45PM on April 5th of this year
               Y-2Y+4M+5D+16h+45m - 4:45PM on April 5th of two years ago
               -5h-30m            - 5 hours and 30 minutes ago

EOF
  exit(0);
}

my $userid = shift(@ARGV);
my $startdate = parse_date(\@ARGV);
my $enddate = parse_date(\@ARGV);
my $userpass = get_password();

if (defined($startdate) && !defined($enddate)) { $enddate = time(); }

my $req_content = make_ofx_request($userid, $userpass, $startdate, $enddate);

my $ua = new LWP::UserAgent();
my $req = new HTTP::Request(POST=>"https://ofx.discovercard.com");
$req->content_type("application/x-ofx");
$req->content($req_content);
my $res = $ua->request($req);
if ($res->is_success) {
  printf("%s\n", $res->content());
} else {
  printf("%s\n", $res->as_string())
}

exit(0);

################################# OFX REQUEST #################################
__DATA__
<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<?OFX OFXHEADER="200" VERSION="200" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>

<OFX>
 <SIGNONMSGSRQV1>
  <SONRQ>
   <DTCLIENT>20010101010203</DTCLIENT>
   <USERID>6011XXXXYYYYZZZZ</USERID>
   <USERPASS>Password</USERPASS>
   <LANGUAGE>ENG</LANGUAGE>
   <APPID>QWIN</APPID>
   <APPVER>1100</APPVER>
  </SONRQ>
 </SIGNONMSGSRQV1>

 <CREDITCARDMSGSRQV1>
  <CCSTMTTRNRQ>
   <TRNUID>gyXJ5Tz4sH7xAU99XIUw</TRNUID>
   <CCSTMTRQ>
    <CCACCTFROM>
     <ACCTID>6011XXXXYYYYZZZZ</ACCTID>
    </CCACCTFROM>
    <INCTRAN>
     <DTSTART>20010101010203</DTSTART>
     <DTEND>20010101010203</DTEND>
     <INCLUDE>Y</INCLUDE>
    </INCTRAN>
   </CCSTMTRQ>
  </CCSTMTTRNRQ>
 </CREDITCARDMSGSRQV1>
</OFX>
