File Coverage

blib/lib/Finance/Card/Discover/Account.pm
Criterion Covered Total %
statement 15 81 18.5
branch 0 28 0.0
condition 0 11 0.0
subroutine 5 13 38.4
pod 5 6 83.3
total 25 139 17.9


line stmt bran cond sub pod time code
1             package Finance::Card::Discover::Account;
2              
3 1     1   751 use strict;
  1         2  
  1         37  
4 1     1   5 use warnings;
  1         1  
  1         32  
5              
6 1     1   5 use Carp qw(croak);
  1         2  
  1         66  
7 1     1   1069 use DateTime::Tiny;
  1         805  
  1         27  
8 1         6 use Object::Tiny qw(
9             card credit expiration id nickname number type
10 1     1   739 );
  1         295  
11              
12             our $XML_PARSER;
13              
14             sub new {
15 0     0 0   my ($class, $data, $num, %params) = @_;
16              
17 0           my ($year, $month) = split '/', $data->{"expiry${num}"}, 2;
18 0 0         $year += 2000 if 2000 > $year;
19 0           my $expiration = DateTime::Tiny->new(year => $year, month => $month);
20              
21 0           return bless {
22             card => $params{card},
23             credit => $data->{"AccountOpenToBuy${num}"},
24             expiration => $expiration,
25             id => $data->{"cardsubid${num}"},
26             nickname => $data->{"nickname$num"},
27             number => $data->{"pan${num}"},
28             type => $data->{"cardtype${num}"},
29             }, $class;
30             }
31              
32             sub balance {
33 0     0 1   my $dom = $_[0]->_ofx_request;
34 0 0         return unless $dom;
35 0           $dom->findvalue('//CCSTMTTRNRS/CCSTMTRS/LEDGERBAL/BALAMT');
36             }
37              
38             sub transactions {
39 0     0 1   my ($self, %params) = @_;
40              
41 0           for my $param (qw(start end)) {
42 0 0         next unless exists $params{$param};
43 0   0       my $type = ref $params{$param} || '';
44 0 0         croak "'$param' must be a DateTime or DateTime::Tiny, not $type"
45             unless $type =~ m[^DateTime(?:::Tiny)$];
46             }
47              
48 0           my $dom = $self->_ofx_request(%params, transactions => 1);
49 0 0         return unless $dom;
50              
51 0           require Finance::Card::Discover::Account::Transaction;
52              
53 0           my @transactions;
54 0           for my $node ($dom->findnodes('//BANKTRANLIST/STMTTRN')) {
55 0           my ($type, $date, $amount, $id, $name) = map {
56 0           $node->findvalue($_)
57             } qw(TRNTYPE DTPOSTED TRNAMT FITID NAME);
58 0           my ($year, $month, $day) = unpack 'A4A2A2', $date;
59 0           $date = DateTime::Tiny->new(year=>$year, month=>$month, day=>$day);
60 0           my $transaction = Finance::Card::Discover::Account::Transaction->new(
61             type => lc $type,
62             date => $date,
63             amount => $amount,
64             id => $id,
65             name => $name,
66             );
67 0           push @transactions, $transaction;
68             }
69              
70 0           return @transactions;
71             }
72              
73             sub _ofx_request {
74 0     0     my ($self, %params) = @_;
75              
76 0           my $dt = _dt_to_ofx(DateTime::Tiny->now);
77              
78 0           my ($trans, $start, $end);
79 0 0         if ($trans = $params{transactions}) {
80 0           ($start, $end) = @params{qw(start end)};
81 0   0       $_ &&= _dt_to_ofx($_) for ($start, $end);
82             }
83              
84 0           my $xml = <<" __EOF__";
85            
86            
87             NEWFILEUID="NONE"?>
88            
89            
90            
91             $dt
92 0           @{[ $self->card->{username } ]}
  0            
93 0           @{[ $self->card->{password} ]}
94             ENG
95             Discover Financial Services7101
96             QWIN1800
97            
98            
99            
100            
101             ${$}_$dt
102            
103 0 0         @{[ $self->number ]}
104            
105 0 0         @{[ $start ? "$start" : '' ]}
106 0 0         @{[ $end ? "$end" : '' ]}
107             @{[ $trans ? 'Y' : 'N' ]}
108            
109            
110            
111            
112            
113             __EOF__
114              
115 0           my $ua = $self->card->ua;
116 0           my $uri = URI->new('https://ofx.discovercard.com/');
117 0           my $res = $self->card->{response} = $ua->post(
118             $uri,
119 0           if_ssl_cert_subject => "/CN=(?i)\Q@{[$uri->host]}\E\$",
120             content_type => 'application/x-ofx',
121             content => $xml,
122             );
123 0 0         return unless $res->is_success;
124              
125 0           require XML::LibXML;
126 0   0       $XML_PARSER ||= XML::LibXML->new;
127 0 0         my $dom = eval {
128 0           $XML_PARSER->parse_string($res->decoded_content);
129             } or croak "Failed to parse response XML: $@";
130 0           return $dom;
131             }
132              
133             sub _dt_to_ofx {
134 0     0     my ($dt) = @_;
135 0           sprintf '%d%02d%02d%02d%02d%02d.000', $dt->year, $dt->month,
136             $dt->day, $dt->hour, $dt->minute, $dt->second;
137             }
138              
139             sub profile {
140 0     0 1   my ($self) = @_;
141              
142 0           my $data = $self->card->_request(
143             cardsubid => $self->id,
144             cardtype => $self->type,
145             msgnumber => 0,
146             profilename => 'billing',
147             request => 'getprofile',
148             );
149 0 0         return unless $data;
150              
151 0           require Finance::Card::Discover::Account::Profile;
152 0           return Finance::Card::Discover::Account::Profile->new(
153             $data, account => $self
154             );
155             }
156              
157             sub soan {
158 0     0 1   my ($self) = @_;
159              
160 0           my $data = $self->card->_request(
161             cardsubid => $self->id,
162             cardtype => $self->type,
163             clienttype => 'thin',
164             cpntype => 'MA', # ?
165             latched => 'Y', # ?
166             msgnumber => 2,
167             request => 'ocode',
168              
169             # TODO: test to see if this setting alters the expiration from the
170             # default value. Currently, a user must call or send a message to
171             # DiscoverCard to cancel a SOAN.
172             validfor => undef,
173             );
174 0 0         return unless $data;
175              
176 0           require Finance::Card::Discover::Account::SOAN;
177 0           return Finance::Card::Discover::Account::SOAN->new(
178             $data, account => $self
179             );
180             }
181              
182             sub soan_transactions {
183 0     0 1   my ($self) = @_;
184              
185 0           my $data = $self->card->_request(
186             cardtype => $self->type,
187             cardsubid => $self->id,
188             msgnumber => 1,
189             request => 'ocodereview',
190              
191             # These might be useful.
192             maxtrans => undef,
193             fromdate => undef,
194             todate => undef,
195             );
196 0 0 0       return unless $data and $data->{Total};
197              
198 0           require Finance::Card::Discover::Account::SOAN::Transaction;
199 0           return map {
200 0           Finance::Card::Discover::Account::SOAN::Transaction->new(
201             $data, $_, soan => $self
202             );
203             } (1 .. $data->{Total});
204             }
205              
206              
207             1;
208              
209             __END__