File Coverage

blib/lib/Finance/Card/Citibank.pm
Criterion Covered Total %
statement 21 147 14.2
branch 0 42 0.0
condition 0 7 0.0
subroutine 7 18 38.8
pod 1 1 100.0
total 29 215 13.4


line stmt bran cond sub pod time code
1             package Finance::Card::Citibank;
2              
3             # ABSTRACT: Check your credit card balances.
4              
5 2     2   48183 use strict;
  2         3  
  2         65  
6 2     2   8 use warnings;
  2         3  
  2         49  
7              
8 2     2   8 use Carp;
  2         7  
  2         150  
9 2     2   1658 use LWP;
  2         119830  
  2         64  
10 2     2   2598 use DateTime;
  2         360079  
  2         145  
11 2     2   3813 use HTML::Parser;
  2         23482  
  2         5629  
12              
13             our $VERSION = '2.02';
14              
15             my $ua = LWP::UserAgent->new();
16              
17             sub check_balance {
18 0     0 1   my ( $class, %opts ) = @_;
19 0           my $self = bless {%opts}, $class;
20              
21 0           my $position = 1;
22 0           my @accounts;
23              
24 0           my @ofx_accounts = $self->_get_accounts;
25 0           for my $accnt (@ofx_accounts) {
26              
27 0           my $acctid = $accnt->{ccacctinfo}{ccacctfrom}{acctid};
28 0           my $desc = $accnt->{desc};
29             # print "id: $acctid\n";
30             # print "desc: $desc\n";
31              
32 0           my $balance =
33             $self->_get_account_balance(
34             $accnt->{ccacctinfo}{ccacctfrom}{acctid} );
35             # print "balance: $balance\n";
36              
37 0           push @accounts, (
38             bless {
39             balance => $balance,
40             name => $desc,
41             sort_code => $acctid,
42             account_no => $acctid,
43             position =>
44             $position++, # redundant since just = array index + 1
45             statement => undef,
46             ## parent => $self,
47             },
48             "Finance::Card::Citibank::Account"
49             );
50              
51             }
52              
53 0           return @accounts;
54             }
55              
56             sub _get_accounts {
57 0     0     my $self = shift;
58              
59 0           my $content = $self->_retrive_accounts;
60              
61 0           my ( $ofx_header, $ofx_body ) = split /\n\n/, $content, 2;
62 0           my $tree = $self->_parse( $content );
63              
64 0           my $accntinfo =
65             $tree->{ofx}{signupmsgsrsv1}{acctinfotrnrs}{acctinfors}{acctinfo};
66 0 0         my @accounts = ref $accntinfo eq 'ARRAY' ? @$accntinfo : $accntinfo;
67              
68 0           return @accounts;
69             }
70              
71             sub _get_account_balance {
72 0     0     my ( $self, $account ) = @_;
73              
74 0           my $content = $self->_retrive_account_balance($account);
75 0           my $tree = $self->_parse( $content );
76              
77 0 0         exists $tree->{ofx}{creditcardmsgsrsv1}{ccstmttrnrs}{ccstmtrs}{ledgerbal}
78             {balamt}
79             or confess "Unable to find balance: $content";
80 0           my $balance =
81             $tree->{ofx}{creditcardmsgsrsv1}{ccstmttrnrs}{ccstmtrs}{ledgerbal}
82             {balamt};
83              
84 0           return $balance;
85             }
86              
87             sub _retrive_accounts {
88 0     0     my $self = shift;
89              
90 0 0         if ( $self->{content} ) {
91              
92             # If we give it a file, use the file rather than downloading
93 0 0         open my $fh, "<", $self->{content} or confess;
94 0           my $content = do { local $/ = undef; <$fh> };
  0            
  0            
95 0           close $fh;
96 0           return $content;
97             }
98              
99 0 0         croak "Must provide a password" unless exists $self->{password};
100 0 0         croak "Must provide a username" unless exists $self->{username};
101              
102 0           my $r =
103             HTTP::Request->new( POST =>
104             'https://secureofx2.bankhost.com/citi/cgi-forte/ofx_rt?servicename=ofx_rt&pagename=ofx'
105             );
106 0           $r->content_type('application/x-ofx');
107 0           $r->content( <<"ACCNT_REQ" );
108             OFXHEADER:100
109             DATA:OFXSGML
110             VERSION:102
111             SECURITY:NONE
112             ENCODING:USASCII
113             CHARSET:1252
114             COMPRESSION:NONE
115             OLDFILEUID:NONE
116             NEWFILEUID:NONE
117              
118            
119            
120            
121 0           @{[ DateTime->now->strftime('%Y%m%d%H%M%S.000') ]}
122 0           @{[ $self->{username } ]}
  0            
123 0           @{[ $self->{password} ]}
124             ENG
125            
126             Citigroup
127             24909
128            
129             QWIN
130             1800
131            
132            
133            
134            
135             @{[ DateTime->now->strftime('%Y%m%d%H%M%S.000') ]}
136             1
137            
138             19691231
139            
140            
141            
142            
143             ACCNT_REQ
144              
145             # print "request: ", $r->as_string, "\n\n---\n\n";
146 0           my $response = $ua->request($r);
147 0           my $content = $response->content;
148              
149 0 0         if ( $self->{log} ) {
150              
151             # Dump to the filename passed in log
152 0 0         open( my $fh, ">", $self->{log} ) or confess;
153 0           print $fh $content;
154 0           close $fh;
155             }
156              
157 0           return $content;
158              
159             }
160              
161             sub _retrive_account_balance {
162 0     0     my ( $self, $account ) = @_;
163              
164 0 0         if ( $self->{content2} ) {
165              
166             # If we give it a file, use the file rather than downloading
167 0 0         open my $fh, "<", $self->{content2} or confess;
168 0           my $content = do { local $/ = undef; <$fh> };
  0            
  0            
169 0           close $fh;
170 0           return $content;
171             }
172              
173 0 0         croak "Must provide a password" unless exists $self->{password};
174 0 0         croak "Must provide a username" unless exists $self->{username};
175              
176 0           my $r =
177             HTTP::Request->new( POST =>
178             'https://secureofx2.bankhost.com/citi/cgi-forte/ofx_rt?servicename=ofx_rt&pagename=ofx'
179             );
180 0           $r->content_type('application/x-ofx');
181 0           $r->content( <<"ACCNT_REQ" );
182             OFXHEADER:100
183             DATA:OFXSGML
184             VERSION:102
185             SECURITY:NONE
186             ENCODING:USASCII
187             CHARSET:1252
188             COMPRESSION:NONE
189             OLDFILEUID:NONE
190             NEWFILEUID:NONE
191              
192            
193            
194            
195 0           @{[ DateTime->now->strftime('%Y%m%d%H%M%S.000') ]}
196 0           @{[ $self->{username } ]}
  0            
197 0           @{[ $self->{password} ]}
198             ENG
199            
200             Citigroup
201             24909
202            
203             QWIN
204             1800
205            
206            
207            
208            
209 0           @{[ DateTime->now->strftime('%Y%m%d%H%M%S.000') ]}
210             1
211            
212            
213             @{[ $account ]}
214            
215            
216             19691231
217             N
218            
219            
220            
221            
222            
223             ACCNT_REQ
224              
225             # print "request: ", $r->as_string, "\n\n---\n\n";
226 0           my $response = $ua->request($r);
227 0           my $content = $response->content;
228              
229 0 0         if ( $self->{log2} ) {
230              
231             # Dump to the filename passed in log
232 0 0         open( my $fh, ">", $self->{log2} ) or confess;
233 0           print $fh $content;
234 0           close $fh;
235             }
236              
237 0           return $content;
238              
239             }
240              
241             sub _parse {
242 0     0     my ($self,$content) = @_;
243              
244 0           my ( $ofx_header, $ofx_body ) = split /\n\n/, $content, 2;
245              
246 0           my @tree;
247             my @stack;
248 0           unshift @stack, \@tree;
249              
250             my $p = HTML::Parser->new(
251             start_h => [
252             sub {
253 0     0     my $data = shift;
254              
255 0           my @content = ();
256 0           push @{ $stack[0] }, { name => $data, content => \@content };
  0            
257 0           unshift @stack, \@content;
258             },
259             'tagname'
260             ],
261             end_h => [
262             sub { # An end event unwinds the stack by one level
263 0     0     shift(@stack);
264             },
265             ''
266             ],
267             text_h => [
268             sub {
269 0     0     my $data = shift;
270 0           $data =~ s/^\s*//; # Strip leading whitespace
271 0           $data =~ s/\s*$//; # Strip trailing whitespace
272 0 0         return unless length $data; # Ignore empty strings
273 0 0         if ( scalar( @{ $stack[0] } ) ) {
  0            
274 0           print STDERR "Naked text\n";
275 0           return;
276             }
277 0           shift @stack; # Unwind the vestigal array reference
278 0           @{ $stack[0] }[-1]->{content} = $data;
  0            
279             },
280 0           'dtext'
281             ] );
282 0           $p->unbroken_text(1); # Want element contents in single blocks to facilita
283 0           $p->parse($ofx_body);
284              
285 0           my $tree = _collapse(\@tree);
286 0           my $resp_code = $tree->{ofx}{signonmsgsrsv1}{sonrs}{status}{code};
287 0 0 0       if ( undef $resp_code or $resp_code ) { # Undef or not 0
288 0           confess "Error in response from ofx server: $ofx_body";
289             }
290              
291 0           return $tree;
292              
293             }
294              
295             sub _is_unique {
296 0     0     my $a = shift;
297 0 0         return undef unless ref($a) eq 'ARRAY';
298 0           my %saw;
299 0   0       $saw{ $_->{name} }++ || return 0 for @{$a};
  0            
300 0           1;
301             }
302              
303             sub _collapse {
304 0     0     my $tree = shift;
305 0 0         return $tree unless ref($tree) eq 'ARRAY';
306              
307             # Recurse on any elements that have arrays for content
308 0           $_->{content} = _collapse( $_->{content} ) for ( @{$tree} );
  0            
309              
310             # The passed array can be converted to a hash if all of it's nodes have
311             # unique names
312 0           my %a;
313 0 0         if ( _is_unique($tree) ) {
314 0           $a{ $_->{name} } = $_->{content} for ( @{$tree} );
  0            
315             } else # Duplicate names can be converted to an array
316             {
317 0           my %b;
318 0           $b{ $_->{name} }++ for @{$tree};
  0            
319              
320             # grep(!$b{$_->{name}}++, @{$tree});
321 0   0       ( $b{$_} > 1 ) && ( $a{$_} = [] ) for keys %b;
322 0           for ( @{$tree} ) {
  0            
323 0 0         push( @{ $a{ $_->{name} } }, $_->{content} ), next
  0            
324             if $b{ $_->{name} } > 1;
325 0           $a{ $_->{name} } = $_->{content};
326              
327             # ($b{$_->{name}} > 1) ? push(@{$a{$_->{name}}}, $_->{content}) :
328             # ($a{$_->{name}} = $_->{content});
329             }
330             }
331 0           return \%a;
332             }
333              
334             package Finance::Card::Citibank::Account;
335 2     2   23 use base qw(Class::Accessor::Fast);
  2         5  
  2         3443  
336             __PACKAGE__->mk_accessors(
337             qw(balance name sort_code account_no position statement));
338              
339             1;
340              
341             __END__