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__ |