blib/lib/Finance/Bank/IE/BankOfIreland.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 9 | 9 | 100.0 |
branch | n/a | ||
condition | n/a | ||
subroutine | 3 | 3 | 100.0 |
pod | n/a | ||
total | 12 | 12 | 100.0 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | =head1 NAME | ||||||
2 | |||||||
3 | Finance::Bank::IE::BankOfIreland - Interface to Bank of Ireland online banking | ||||||
4 | |||||||
5 | =head1 SYNOPSIS | ||||||
6 | |||||||
7 | use Finance::Bank::IE::BankOfIreland; | ||||||
8 | |||||||
9 | # config | ||||||
10 | my $conf = { user => '', pin => '', contact => '', dob => '' }; | ||||||
11 | |||||||
12 | # get balance from all accounts | ||||||
13 | my @accounts = Finance::Bank::IE::BankOfIreland->check_balance( $conf ); | ||||||
14 | |||||||
15 | # get account transaction details | ||||||
16 | my @details = Finance::Bank::IE::BankOfIreland->account_details( $acct ); | ||||||
17 | |||||||
18 | # list beneficiaries for an account | ||||||
19 | my $bene = Finance::Bank::IE::BankOfIreland->list_beneficiaries( $acct ); | ||||||
20 | |||||||
21 | # transfer money to a beneficiary | ||||||
22 | my $tx = Finance::Bank::IE::BankOfIreland->funds_transfer( $from, $to, $amt ); | ||||||
23 | |||||||
24 | =head1 DESCRIPTION | ||||||
25 | |||||||
26 | Module to interact with BoI's 365 Online service. | ||||||
27 | |||||||
28 | =head1 FUNCTIONS | ||||||
29 | |||||||
30 | Note that all functions are set up to act as methods (i.e. they all need to be invoked using F:B:I:B->method()). All functions also take an optional configuration hash as a final parameter. | ||||||
31 | |||||||
32 | =over | ||||||
33 | |||||||
34 | =cut | ||||||
35 | package Finance::Bank::IE::BankOfIreland; | ||||||
36 | |||||||
37 | |||||||
38 | 3 | 3 | 137749 | use strict; | |||
3 | 7 | ||||||
3 | 118 | ||||||
39 | 3 | 3 | 17 | use warnings; | |||
3 | 5 | ||||||
3 | 152 | ||||||
40 | |||||||
41 | our $VERSION = "0.30"; | ||||||
42 | |||||||
43 | 3 | 3 | 17 | use base qw( Finance::Bank::IE ); | |||
3 | 12 | ||||||
3 | 2237 | ||||||
44 | use POSIX; | ||||||
45 | |||||||
46 | # headers for account summary page | ||||||
47 | use constant { | ||||||
48 | BALANCE => "Balance Information: Balance", | ||||||
49 | ACCTTYPE => "Account Type", | ||||||
50 | NICKNAME => "Nickname Information: Nickname", | ||||||
51 | CURRENCY => "Currency", | ||||||
52 | ACCTNUM => "Account Number", | ||||||
53 | }; | ||||||
54 | |||||||
55 | # headers for transaction list page | ||||||
56 | use constant { | ||||||
57 | DATE => "Date", | ||||||
58 | DETAIL => "Details", | ||||||
59 | DEBIT => "Debit", | ||||||
60 | CREDIT => "Credit", | ||||||
61 | DETBAL => "Balance Information: Balance", | ||||||
62 | }; | ||||||
63 | |||||||
64 | # headers for payments page | ||||||
65 | use constant { | ||||||
66 | BENNAME => 'Beneficiary Name Information: Beneficiary Name', | ||||||
67 | BENACCT => 'Account Number', | ||||||
68 | BENNSC => | ||||||
69 | 'National Sort Code (NSC) Information: National Sort Code (NSC)', | ||||||
70 | BENREF => 'Reference Number Information: Reference Number', | ||||||
71 | BENDESC => | ||||||
72 | 'Beneficiary Description Information: Beneficiary Description', | ||||||
73 | BENSTATUS => 'Status Information: Status', | ||||||
74 | }; | ||||||
75 | |||||||
76 | my $BASEURL = "https://www.365online.com/"; | ||||||
77 | |||||||
78 | my %pages = ( | ||||||
79 | login => { | ||||||
80 | url => 'https://www.365online.com/online365/spring/authentication', | ||||||
81 | sentinel => 'Login.*Step 1 of 2', | ||||||
82 | }, | ||||||
83 | login2 => { | ||||||
84 | url => 'https://www.365online.com/online365/spring/authentication', | ||||||
85 | sentinel => 'Login.*Step 2 of 2', | ||||||
86 | }, | ||||||
87 | accessDenied => { | ||||||
88 | url => 'https://www.365online.com/online365/spring/accessDenied', | ||||||
89 | sentinel => 'Access Denied', |
||||||
90 | }, | ||||||
91 | badcreds => { | ||||||
92 | url => 'https://www.365online.com/online365/spring/authentication', | ||||||
93 | sentinel => 'Your login details are incorrect, please try again', | ||||||
94 | }, | ||||||
95 | expired => { | ||||||
96 | url => 'https://www.365online.com/online365/spring/sessionExpired', | ||||||
97 | sentinel => 'The system has logged you out', | ||||||
98 | }, | ||||||
99 | # generic interstitial | ||||||
100 | interstitial => { | ||||||
101 | url => 'not important', | ||||||
102 | sentinel => 'Continue to 365 Home', | ||||||
103 | }, | ||||||
104 | termsandconds => { | ||||||
105 | url => 'not important', | ||||||
106 | sentinel => 'Terms and Conditions', |
||||||
107 | }, | ||||||
108 | accounts => { | ||||||
109 | url => 'https://www.365online.com/online365/spring/accountSummary?execution=e2s1', | ||||||
110 | sentinel => '>Your Accounts\b', | ||||||
111 | }, | ||||||
112 | statements => { | ||||||
113 | url => 'https://www.365online.com/online365/spring/statements?execution=e1s1', | ||||||
114 | sentinel => 'Recent Transactions', | ||||||
115 | }, | ||||||
116 | moneyTransfer => { | ||||||
117 | url => 'https://www.365online.com/online365/spring/moneyTransfer?execution=e7s1', | ||||||
118 | sentinel => 'Money Transfer', | ||||||
119 | }, | ||||||
120 | manageaccounts => { | ||||||
121 | url => 'https://www.365online.com/online365/spring/manageAccounts?execution=e5s1', | ||||||
122 | sentinel => 'Manage Your Accounts', | ||||||
123 | }, | ||||||
124 | managepayees => { | ||||||
125 | url => 'https://www.365online.com/online365/spring/managePayees?execution=e6s1', | ||||||
126 | sentinel => 'Manage Payees', | ||||||
127 | }, | ||||||
128 | ); | ||||||
129 | |||||||
130 | use HTML::TokeParser; | ||||||
131 | use Carp; | ||||||
132 | use Date::Parse; | ||||||
133 | use POSIX; | ||||||
134 | use File::Path; | ||||||
135 | use Data::Dumper; | ||||||
136 | |||||||
137 | sub _pages { | ||||||
138 | return \%pages; | ||||||
139 | } | ||||||
140 | |||||||
141 | # =item * login_dance( $config ); | ||||||
142 | |||||||
143 | # Logs in or refreshes the current session. The config parameter is a hash reference which is cached the first time it is used, so can be omitted thereafter. The contents of the hash are the login details for your 365 Online account: | ||||||
144 | |||||||
145 | # =over | ||||||
146 | |||||||
147 | # =item * user: your six-digit BoI user ID | ||||||
148 | |||||||
149 | # =item * pin: your six-digit PIN | ||||||
150 | |||||||
151 | # =item * contact: the last four digits of your contact number | ||||||
152 | |||||||
153 | # =item * dob: your date of birth in DD/MM/YYYY format | ||||||
154 | |||||||
155 | # =back | ||||||
156 | |||||||
157 | # No validation is currently done on the format of the config items. The function returns true or false. Note that this function should rarely need to be directly used as it's invoked by the other functions as a first step. | ||||||
158 | |||||||
159 | # =cut | ||||||
160 | # sub login_dance { | ||||||
161 | # my $self = shift; | ||||||
162 | # my $confref = shift; | ||||||
163 | |||||||
164 | # confess(); | ||||||
165 | |||||||
166 | # $confref ||= $self->cached_config(); | ||||||
167 | |||||||
168 | # for my $required ( "user", "pin", "contact", "dob" ) { | ||||||
169 | # if ( !defined( $confref->{$required} )) { | ||||||
170 | # $self->_dprintf( "$required not specified\n" ); | ||||||
171 | # return; | ||||||
172 | # } | ||||||
173 | # } | ||||||
174 | |||||||
175 | # $self->cached_config( $confref ); | ||||||
176 | |||||||
177 | # if ( !$self->_get( $pages{login}->{url}, $confref )) { | ||||||
178 | # croak( "Failed to get login page." ); | ||||||
179 | # } | ||||||
180 | |||||||
181 | # # TODO check sentinel & form name | ||||||
182 | # my $form = $self->_agent()->current_form(); | ||||||
183 | # $self->_agent()->field( "form:userId", $confref->{user} ); | ||||||
184 | # $self->_set_creds_fields( $confref ); | ||||||
185 | # my $res = $self->_agent()->submit_form(); | ||||||
186 | # $self->_identify_page(); | ||||||
187 | # $self->_save_page(); | ||||||
188 | |||||||
189 | # if ( !$res->is_success ) { | ||||||
190 | # croak( "Failed to submit login form" ); | ||||||
191 | # } | ||||||
192 | |||||||
193 | # $self->_set_creds_fields( $confref ); | ||||||
194 | # $res = $self->_agent()->submit_form(); | ||||||
195 | # $self->_identify_page(); | ||||||
196 | # $self->_save_page(); | ||||||
197 | |||||||
198 | # if ( !$res->is_success ) { | ||||||
199 | # croak( "Failed to submit login form" ); | ||||||
200 | # } | ||||||
201 | |||||||
202 | # if ( $res->content() =~ /$pages{badcreds}->{sentinel}/s ) { | ||||||
203 | # croak "Your login details are incorrect"; | ||||||
204 | # } elsif ( $res->content() =~ /$pages{login}->{sentinel}/s ) { | ||||||
205 | # croak( "Looping, bailing out to avoid lockout\n" ); | ||||||
206 | # } | ||||||
207 | |||||||
208 | # # one other fail string: You did not enter the 3 requested digits of your PIN! | ||||||
209 | |||||||
210 | # # GAH INTERSTITIALS | ||||||
211 | # if ( $self->_identify_page eq 'interstitial' ) { | ||||||
212 | # $self->_agent()->field( "form:continue", "form:continue" ); | ||||||
213 | # $res = $self->_agent()->submit_form(); | ||||||
214 | # $self->_identify_page(); | ||||||
215 | # $self->_save_page(); | ||||||
216 | # } | ||||||
217 | |||||||
218 | # return 1; | ||||||
219 | # } | ||||||
220 | |||||||
221 | sub _submit_first_login_page { | ||||||
222 | my $self = shift; | ||||||
223 | my $confref = shift||$self->cached_config(); | ||||||
224 | |||||||
225 | my $form = $self->_agent()->current_form(); | ||||||
226 | $self->_agent()->field( "form:userId", $confref->{user} ); | ||||||
227 | $self->_set_creds_fields( $confref ); | ||||||
228 | return $self->_agent()->submit_form(); | ||||||
229 | } | ||||||
230 | |||||||
231 | sub _submit_second_login_page { | ||||||
232 | my $self = shift; | ||||||
233 | my $confref = shift||$self->cached_config(); | ||||||
234 | |||||||
235 | $self->_set_creds_fields( $confref ); | ||||||
236 | return $self->_agent()->submit_form(); | ||||||
237 | } | ||||||
238 | |||||||
239 | =item * $self->check_balance() | ||||||
240 | |||||||
241 | Fetch all account balances from the account summary page. Returns an array of Finance::Bank::IE::BankOfIreland::Account objects. | ||||||
242 | |||||||
243 | =cut | ||||||
244 | |||||||
245 | sub check_balance { | ||||||
246 | my $self = shift; | ||||||
247 | my $confref = shift; | ||||||
248 | |||||||
249 | $confref ||= $self->cached_config(); | ||||||
250 | $self->_get( $self->_pages->{accounts}->{url}, $confref ); | ||||||
251 | |||||||
252 | if ( $self->_agent()->content() !~ /$pages{accounts}->{sentinel}/s ) { | ||||||
253 | croak( "Failed to get account summary page" ); | ||||||
254 | } | ||||||
255 | |||||||
256 | my $summary = $self->_agent()->content; | ||||||
257 | my $parser = new HTML::TokeParser( \$summary ); | ||||||
258 | |||||||
259 | my ( @accounts, %account, @headings ); | ||||||
260 | my ( $getheadings, $col ) = ( 1, 0 ); | ||||||
261 | |||||||
262 | while ( my $tag = $parser->get_tag( "span" )) { | ||||||
263 | if ( $self->_streq( $tag->[1]{class}, "acc_name" )) { | ||||||
264 | # ugh. accountname | ||||||
265 | while ( my $token = $parser->get_token()) { | ||||||
266 | if ( $token->[0] eq 'T' ) { | ||||||
267 | ( $account{+NICKNAME} = $token->[1] ) =~ s/\s+$//; | ||||||
268 | last; | ||||||
269 | } | ||||||
270 | } | ||||||
271 | $account{+ACCTNUM} = $parser->get_trimmed_text( "/span"); | ||||||
272 | } elsif ( $self->_streq( $tag->[1]{class}, "acc_value" )) { | ||||||
273 | $account{+CURRENCY} = $parser->get_trimmed_text( "/span" ); | ||||||
274 | $parser->get_tag( "span" ); | ||||||
275 | $account{+BALANCE} = $parser->get_trimmed_text; | ||||||
276 | |||||||
277 | push @accounts, | ||||||
278 | bless { | ||||||
279 | type => delete $account{+ACCTTYPE}, | ||||||
280 | nick => delete $account{+NICKNAME}, | ||||||
281 | account_no => delete $account{+ACCTNUM}, | ||||||
282 | currency => delete $account{+CURRENCY}, | ||||||
283 | balance => delete $account{+BALANCE}, | ||||||
284 | }, "Finance::Bank::IE::BankOfIreland::Account"; | ||||||
285 | } | ||||||
286 | } | ||||||
287 | |||||||
288 | if ( !@accounts ) { | ||||||
289 | $self->_dprintf( "No accounts found\n" ); | ||||||
290 | } | ||||||
291 | |||||||
292 | return @accounts; | ||||||
293 | } | ||||||
294 | |||||||
295 | =item * $self->account_details( account [,config] ) | ||||||
296 | |||||||
297 | Return transaction details from the specified account | ||||||
298 | |||||||
299 | =cut | ||||||
300 | sub account_details { | ||||||
301 | my $self = shift; | ||||||
302 | my $account = shift; | ||||||
303 | my $confref = shift; | ||||||
304 | my ( @headings, @details ); | ||||||
305 | |||||||
306 | $confref ||= $self->cached_config(); | ||||||
307 | |||||||
308 | my $content = $self->_get( $pages{statements}->{url}, $confref ); | ||||||
309 | |||||||
310 | if ( !$content) { | ||||||
311 | croak( "Failed to get account summary page" ); | ||||||
312 | } | ||||||
313 | |||||||
314 | # account selector | ||||||
315 | my $error = 'account not found'; | ||||||
316 | my $parser = new HTML::TokeParser( \$content ); | ||||||
317 | |||||||
318 | while ( my $tag = $parser->get_tag( "select" )) { | ||||||
319 | if ( $self->_streq( "form:selectAccountDropDown", $tag->[1]{id})) { | ||||||
320 | while ( my $optiontag = $parser->get_tag("/select", "option")) { | ||||||
321 | last if $optiontag->[0] eq '/select'; | ||||||
322 | my $accountname = $parser->get_trimmed_text( "/option" ); | ||||||
323 | next if $accountname =~ /Select Account/; | ||||||
324 | my ( $nick, $number ) = split( /\s*~\s*/, $accountname ); | ||||||
325 | $self->_dprintf( "Found account '$nick' = '$number'\n" ); | ||||||
326 | if ( $account eq $nick or $account eq $number or | ||||||
327 | $account eq '~'.$number ) { | ||||||
328 | if ( $self->_streq( $optiontag->[1]{selected}, "selected" )) { | ||||||
329 | $error = 'ok'; | ||||||
330 | } else { | ||||||
331 | $error = 'account not selected'; | ||||||
332 | } | ||||||
333 | last; | ||||||
334 | } | ||||||
335 | } | ||||||
336 | last; | ||||||
337 | } | ||||||
338 | } | ||||||
339 | |||||||
340 | if ( $error ne 'ok' ) { | ||||||
341 | croak( "Account '$account': " . $error ); | ||||||
342 | } | ||||||
343 | |||||||
344 | # now pull out the stuff we were looking for | ||||||
345 | while ( my $tag = $parser->get_tag( "table" )) { | ||||||
346 | if ( $self->_streq( "form:transactionDataTable" ), $tag->[1]{id}) { | ||||||
347 | while ( my $row = $parser->get_tag( "/table", "tr" )) { | ||||||
348 | last if $row->[0] eq '/table'; | ||||||
349 | my @row; | ||||||
350 | while ( my $col = $parser->get_tag( "/tr", "th", "td" )) { | ||||||
351 | last if $col->[0] eq '/tr'; | ||||||
352 | push @row, $parser->get_trimmed_text( "/" . $col->[0] ); | ||||||
353 | } | ||||||
354 | if ( !@headings ) { | ||||||
355 | # Date, Details, Debit, Credit, Balance | ||||||
356 | @headings = @row; | ||||||
357 | } else { | ||||||
358 | # fixups of raw data: | ||||||
359 | my ( $date, $details, $dr, $cr, $balance ) = @row; | ||||||
360 | if ( !$date ) { | ||||||
361 | if ( @details ) { | ||||||
362 | $date = $details[-1]->[0]; | ||||||
363 | } else { | ||||||
364 | $date = 0; # can't be helped | ||||||
365 | } | ||||||
366 | } else { | ||||||
367 | my ( $d, $m, $y ) = split( "/", $date ); | ||||||
368 | # strftime ilk | ||||||
369 | $date = strftime( "%s", 0, 0, 0, $d, $m - 1, $y - 1900, ); | ||||||
370 | } | ||||||
371 | |||||||
372 | $dr ||= 0.0; | ||||||
373 | $cr ||= 0.0; | ||||||
374 | |||||||
375 | $balance ||= ( @details ? $details[-1]->[-1] : 0 ) - $dr + $cr; | ||||||
376 | push @details, [ $date, $details, $dr, $cr, $balance ]; | ||||||
377 | } | ||||||
378 | } | ||||||
379 | last; | ||||||
380 | } | ||||||
381 | } | ||||||
382 | |||||||
383 | return \@headings, @details; | ||||||
384 | } | ||||||
385 | |||||||
386 | =item * $self->list_beneficiaries( account ) | ||||||
387 | |||||||
388 | List beneficiaries of C |
||||||
389 | |||||||
390 | =cut | ||||||
391 | sub list_beneficiaries { | ||||||
392 | my $self = shift; | ||||||
393 | my $account_from = shift; | ||||||
394 | my $confref = shift; | ||||||
395 | |||||||
396 | $self->_dprintf( "Fetching beneficiaries for %s\n", ref $account_from ? $account_from->{nick} : $account_from ); | ||||||
397 | |||||||
398 | $confref ||= $self->cached_config(); | ||||||
399 | |||||||
400 | # allow passing in of account objects | ||||||
401 | if ( ref $account_from eq "Finance::Bank::IE::BankOfIreland::Account" ) { | ||||||
402 | $account_from = $account_from->{nick}; | ||||||
403 | } | ||||||
404 | |||||||
405 | my $res = | ||||||
406 | $self->_get( $pages{manageaccounts}->{url}, $confref ); | ||||||
407 | $self->_save_page(); | ||||||
408 | if ( !$res ) { | ||||||
409 | croak( "Failed to get " . $pages{manageaccounts}->{url} ); | ||||||
410 | } | ||||||
411 | |||||||
412 | # now we have to pretend to be javascript again. | ||||||
413 | $self->_agent()->field( "form:managePayees", "form:managePayees" ); | ||||||
414 | $res = $self->_agent()->submit_form(); | ||||||
415 | $self->_identify_page(); | ||||||
416 | $self->_save_page(); | ||||||
417 | |||||||
418 | if ( !$res->is_success ) { | ||||||
419 | croak( "Failed to submit manageAccounts form" ); | ||||||
420 | } | ||||||
421 | |||||||
422 | # it would be nice to have more payees to test this with | ||||||
423 | my $content = $self->_agent()->content; | ||||||
424 | my $parser = new HTML::TokeParser( \$content ); | ||||||
425 | my @beneficiaries; | ||||||
426 | while ( my $tag = $parser->get_tag( "table" )) { | ||||||
427 | next unless $tag->[1]{id}; | ||||||
428 | next unless $tag->[1]{id} =~ /payee/i; | ||||||
429 | my %beneficiary; | ||||||
430 | my @cols = qw( desc account_no nsc ref currency nick limit ); | ||||||
431 | while ( $tag = $parser->get_tag( "td", "/tr", "/table" )) { | ||||||
432 | if ( $tag->[0] eq "/table" ) { | ||||||
433 | last; | ||||||
434 | } | ||||||
435 | if ( $tag->[0] eq "/tr" and %beneficiary ) { | ||||||
436 | # I don't currently know what an inactive beneficiary | ||||||
437 | # looks like, so I'm flagging them all as Active. | ||||||
438 | push @beneficiaries, bless { | ||||||
439 | type => 'Beneficiary', | ||||||
440 | status => 'Active', | ||||||
441 | }, "Finance::Bank::IE::BankOfIreland::Account"; | ||||||
442 | for my $k ( keys %beneficiary ) { | ||||||
443 | $beneficiaries[-1]->{$k} = $beneficiary{$k}; | ||||||
444 | } | ||||||
445 | |||||||
446 | %beneficiary = (); | ||||||
447 | } else { | ||||||
448 | while ( my $token = $parser->get_token()) { | ||||||
449 | if ( $token->[0] eq "E" and $token->[1] eq "td" ) { | ||||||
450 | last; | ||||||
451 | } elsif ( $token->[0] eq "S" and $token->[1] eq "label" ) { | ||||||
452 | $beneficiary{$cols[0]} = $parser->get_trimmed_text( "/label" ); | ||||||
453 | } elsif ( $token->[0] eq "T" ) { | ||||||
454 | my $idx = scalar( keys %beneficiary ) - 1; | ||||||
455 | $beneficiary{$cols[$idx]} = $token->[1]; | ||||||
456 | } elsif ( $token->[0] eq "S" and $token->[1] eq 'input' ) { | ||||||
457 | $beneficiary{input} = [ @{$token} ]; | ||||||
458 | } | ||||||
459 | } | ||||||
460 | } | ||||||
461 | } | ||||||
462 | } | ||||||
463 | |||||||
464 | \@beneficiaries; | ||||||
465 | } | ||||||
466 | |||||||
467 | =item * $self->funds_transfer( from, to, amount [,config] ) | ||||||
468 | |||||||
469 | Transfer C |
||||||
470 | |||||||
471 | =cut | ||||||
472 | |||||||
473 | sub funds_transfer { | ||||||
474 | my $self = shift; | ||||||
475 | my $account_from = shift; | ||||||
476 | my $account_to = shift; | ||||||
477 | my $amount = shift; | ||||||
478 | my $confref = shift; | ||||||
479 | |||||||
480 | $self->_dprintf( "Funds transfer of %s from %s to %s\n", $amount, | ||||||
481 | ref $account_from ? $account_from->{nick} : $account_from, | ||||||
482 | ref $account_to ? $account_to->{nick} : $account_to ); | ||||||
483 | |||||||
484 | $confref ||= $self->cached_config(); | ||||||
485 | |||||||
486 | # allow passing in of account objects | ||||||
487 | if ( ref $account_from eq "Finance::Bank::IE::BankOfIreland::Account" ) { | ||||||
488 | $account_from = $account_from->{nick}; | ||||||
489 | } | ||||||
490 | |||||||
491 | if ( ref $account_to eq "Finance::Bank::IE::BankOfIreland::Account" ) { | ||||||
492 | $account_to = $account_to->{nick}; | ||||||
493 | } | ||||||
494 | |||||||
495 | my $beneficiaries = $self->list_beneficiaries( $account_from, $confref ); | ||||||
496 | |||||||
497 | my $acct; | ||||||
498 | for my $bene ( @{$beneficiaries} ) { | ||||||
499 | if ((( $bene->{account_no} ||'' ) eq $account_to ) or | ||||||
500 | (( $bene->{nick} ||'' ) eq $account_to )) { | ||||||
501 | croak "Ambiguous destination account $account_to" | ||||||
502 | if $acct; | ||||||
503 | $acct = $bene; | ||||||
504 | } | ||||||
505 | } | ||||||
506 | |||||||
507 | if ( !defined( $acct )) { | ||||||
508 | croak( "Unable to find $account_to in list of accounts" ); | ||||||
509 | } | ||||||
510 | |||||||
511 | if ( $acct->{status} eq "Inactive" ) { | ||||||
512 | croak( "Inactive beneficiary" ); | ||||||
513 | } | ||||||
514 | |||||||
515 | # now get the funds transfer page | ||||||
516 | my $res = $self->_agent()->get( $pages{moneyTransfer}->{url} ); | ||||||
517 | $self->_save_page(); | ||||||
518 | if ( !$res->is_success ) { | ||||||
519 | croak( "Failed to get funds transfer page." ); | ||||||
520 | } | ||||||
521 | |||||||
522 | # fiddly bit. there are different types of transfer, and I don't | ||||||
523 | # have test accounts to support all of them. | ||||||
524 | # billPayment, ownAccountPayment, domesticPayment, internationalPayment | ||||||
525 | # So, testing the bit I can test. | ||||||
526 | $self->_agent()->field( "form:domesticPayment", "form:domesticPayment" ); | ||||||
527 | $res = $self->_agent()->submit_form(); | ||||||
528 | $self->_identify_page(); | ||||||
529 | $self->_save_page(); | ||||||
530 | croak( 'not on Origin page' ) unless $self->_agent()->content() =~ m@Domestic Transfer@; | ||||||
531 | |||||||
532 | # select the origin account: | ||||||
533 | # select > option > id='form:dt_select_acc_from' | ||||||
534 | # defaults to the right account for me... | ||||||
535 | |||||||
536 | # click on the continue button | ||||||
537 | $res = $self->_agent()->submit_form( button => 'form:formActions:continue' ); | ||||||
538 | $self->_identify_page(); | ||||||
539 | $self->_save_page(); | ||||||
540 | croak( 'not on Details page' ) unless $self->_agent()->content() =~ m@Enter Details@; | ||||||
541 | |||||||
542 | # on this page, there's a single_line_div containing the account name/no | ||||||
543 | # then another one containing the available funds | ||||||
544 | # format is labeldata |
||||||
545 | my $account_selector = ''; | ||||||
546 | my $content = $self->_agent()->content(); | ||||||
547 | my $parser = new HTML::TokeParser( \$content ); | ||||||
548 | my @valid_accounts; | ||||||
549 | while ( my $selector = $parser->get_tag( "select" )) { | ||||||
550 | if ( $self->_streq( $selector->[1]{id}, 'form:selectPayeeDomestic')) { | ||||||
551 | while ( my $option = $parser->get_tag( "option", "/select" )) { | ||||||
552 | last if $option->[0] eq '/select'; | ||||||
553 | my $accountname = $parser->get_trimmed_text( "/option" ); | ||||||
554 | next if $accountname =~ /Select Payee/; | ||||||
555 | push @valid_accounts, $accountname; | ||||||
556 | |||||||
557 | my ( $nick, $number ) = split( /\s*~\s*/, $accountname ); | ||||||
558 | if ( $account_to eq $nick or $account_to eq $number or | ||||||
559 | substr( $account_to, -4 ) eq $number ) { | ||||||
560 | $account_selector = $option->[1]{value}; | ||||||
561 | last; | ||||||
562 | } | ||||||
563 | } | ||||||
564 | last; | ||||||
565 | } | ||||||
566 | } | ||||||
567 | |||||||
568 | if ( $account_selector eq '' ) { | ||||||
569 | croak( sprintf( "Couldn't find payee '%s', valid accounts are '%s'", $account_to, join( "', '", @valid_accounts ))); | ||||||
570 | } | ||||||
571 | |||||||
572 | $res = $self->_agent()->submit_form( | ||||||
573 | fields => { | ||||||
574 | 'form:selectPayeeDomestic' => $account_selector, | ||||||
575 | 'form:amount' => $amount, | ||||||
576 | }, | ||||||
577 | button => 'form:formActions:continue', | ||||||
578 | ); | ||||||
579 | $self->_identify_page(); | ||||||
580 | # also, the destination account number appears in full on this page. | ||||||
581 | $self->_save_page(); | ||||||
582 | croak( 'not on PIN page' ) unless $self->_agent()->content() =~ m@Enter your PIN@; | ||||||
583 | |||||||
584 | $self->_set_creds_fields( $confref ); | ||||||
585 | |||||||
586 | $res = $self->_agent()->submit_form( button => 'form:formActions:continue' ); | ||||||
587 | $self->_identify_page(); | ||||||
588 | $self->_save_page(); | ||||||
589 | croak( 'not on Confirmation page' ) unless $self->_agent()->content() =~ m@Confirmation@s; | ||||||
590 | |||||||
591 | # return the 'receipt' | ||||||
592 | # extraction: | ||||||
593 | # Confirmation |
||||||
594 | # eur AMOUNT |
||||||
595 | # | ||||||
596 | # has been paid from SOURCE to | ||||||
597 | # DEST | ||||||
598 | # | ||||||
599 | return $self->_agent()->content; | ||||||
600 | } | ||||||
601 | |||||||
602 | |||||||
603 | =item * $self->_set_creds_fields( $config ) | ||||||
604 | |||||||
605 | Parse the last received page for credentials entry fields, and populate them with the data from C<$config>. Also injects the missing 'form:continue' hidden field. | ||||||
606 | |||||||
607 | =cut | ||||||
608 | sub _set_creds_fields { | ||||||
609 | my $self = shift; | ||||||
610 | my $confref = shift; | ||||||
611 | |||||||
612 | my $form = $self->_agent()->current_form(); | ||||||
613 | # avoid having to restructure old config | ||||||
614 | my @dob = split( '/', $confref->{dob} ); | ||||||
615 | my %fieldmapping = ( | ||||||
616 | 'form:dateOfBirth_year' => $dob[2], | ||||||
617 | 'form:dateOfBirth_month' => $dob[1], | ||||||
618 | 'form:dateOfBirth_date' => $dob[0], | ||||||
619 | 'form:phoneNumber' => $confref->{contact}, | ||||||
620 | ); | ||||||
621 | for my $i ( 1..6 ) { | ||||||
622 | $fieldmapping{"form:security_number_digit$i"} = substr( $confref->{pin}, $i - 1, 1 ); | ||||||
623 | $fieldmapping{"form:pinFragment:security_number_digit$i"} = substr( $confref->{pin}, $i - 1, 1 ); | ||||||
624 | } | ||||||
625 | |||||||
626 | for my $id ( keys %fieldmapping ) { | ||||||
627 | my $field = $form->find_input( $id ); | ||||||
628 | if ( $field ) { | ||||||
629 | $self->_agent()->field( $id, $fieldmapping{$id}); | ||||||
630 | } | ||||||
631 | } | ||||||
632 | |||||||
633 | # LOSERS. | ||||||
634 | my $input = new HTML::Form::Input( type => 'hidden', | ||||||
635 | name => 'form:continue', | ||||||
636 | value => 'form:continue', | ||||||
637 | ); | ||||||
638 | $input->add_to_form( $form ); | ||||||
639 | } | ||||||
640 | |||||||
641 | =item * $scrubbed = $self->_scrub_page( $content ) | ||||||
642 | |||||||
643 | Scrub the supplied content for PII. | ||||||
644 | |||||||
645 | =cut | ||||||
646 | sub _scrub_page { | ||||||
647 | my ( $self, $content ) = @_; | ||||||
648 | |||||||
649 | my $output = ""; | ||||||
650 | |||||||
651 | my $parser = new HTML::TokeParser( \$content ); | ||||||
652 | my $page = $self->_identify_page( $content ); | ||||||
653 | my $payee_acct = 0; | ||||||
654 | |||||||
655 | while ( my $token = $parser->get_token()) { | ||||||
656 | my $token_string = $token->[0] eq 'T' ? $token->[1] : $token->[-1]; | ||||||
657 | |||||||
658 | if ( $token->[0] eq 'T') { | ||||||
659 | $token_string =~ s@(Last Login.*?)\d+/\d+/\d+ \d+:\d+@${1}01/01/1970 00:00@; | ||||||
660 | $token_string =~ s@(Last Payee Added.*?)\d+/\d+/\d+@${1}01/01/1970@; | ||||||
661 | } | ||||||
662 | |||||||
663 | if ( $token->[0] eq 'S' ) { | ||||||
664 | if ( $token->[1] eq 'h2' ) { | ||||||
665 | my $tpage = ""; | ||||||
666 | while ( my $h2_token = $parser->get_token()) { | ||||||
667 | my $h2_string = $h2_token->[0] eq 'T' ? $h2_token->[1] : $h2_token->[-1]; | ||||||
668 | $token_string .= $h2_string; | ||||||
669 | if ( $h2_token->[0] eq 'E' and $h2_token->[1] eq 'h2' ) { | ||||||
670 | last; | ||||||
671 | } | ||||||
672 | $tpage .= $h2_string; | ||||||
673 | } | ||||||
674 | if ( $tpage ne $page ) { | ||||||
675 | $page = $tpage; | ||||||
676 | } | ||||||
677 | |||||||
678 | # XXX these should use sentinels from %pages | ||||||
679 | if ( $page eq 'Enter your PIN' or $page eq 'Confirmation' ) { | ||||||
680 | # first para contains all the PII, so just nuke it outright | ||||||
681 | $parser->get_tag( "/p" ); | ||||||
682 | } elsif ( $page eq 'Enter Details' ) { | ||||||
683 | # now process until we get past the PII | ||||||
684 | my @replacements = ( 'Nickname ~ 9999', 'eur 99.99' ); | ||||||
685 | while ( @replacements and my $innertoken = $parser->get_token()) { | ||||||
686 | my $itstring = $innertoken->[0] eq 'T' ? $innertoken->[1] : $innertoken->[-1]; | ||||||
687 | if ( $innertoken->[0] eq 'S' and | ||||||
688 | $innertoken->[1] eq 'span' and | ||||||
689 | $self->_streq( $innertoken->[2]{class}, | ||||||
690 | 'pad_txt' )) { | ||||||
691 | my $tag = $parser->get_tag( '/span' ); | ||||||
692 | $itstring .= $tag->[-1]; | ||||||
693 | $parser->get_trimmed_text( '/div' ); | ||||||
694 | $itstring .= shift @replacements; | ||||||
695 | $itstring .= ""; | ||||||
696 | } | ||||||
697 | $token_string .= $itstring; | ||||||
698 | } | ||||||
699 | } | ||||||
700 | } | ||||||
701 | |||||||
702 | if ( $token->[1] eq 'span' ) { | ||||||
703 | if (( $token->[2]{id}||"") =~ /:detailsColumn$/ or | ||||||
704 | ( $self->_streq( $token->[2]{class}, "acc_name" ))) { | ||||||
705 | while ( my $account_token = $parser->get_token()) { | ||||||
706 | if ( $account_token->[0] eq 'T' ) { | ||||||
707 | $token_string .= "Nickname"; | ||||||
708 | last; | ||||||
709 | } else { | ||||||
710 | $token_string .= $account_token->[-1]; | ||||||
711 | } | ||||||
712 | } | ||||||
713 | $parser->get_trimmed_text( "/span"); | ||||||
714 | while ( my $account_token = $parser->get_token()) { | ||||||
715 | if ( $account_token->[0] eq 'T' ) { | ||||||
716 | if ( $account_token->[1] =~ /^(~ *)/ ) { | ||||||
717 | $token_string .= $1; | ||||||
718 | } | ||||||
719 | $token_string .= '9999'; | ||||||
720 | last; | ||||||
721 | } else { | ||||||
722 | $token_string .= $account_token->[-1]; | ||||||
723 | } | ||||||
724 | } | ||||||
725 | $parser->get_trimmed_text( "/span" ); | ||||||
726 | } | ||||||
727 | } elsif ( $self->_streq( $token->[2]{class}, "acc_value" )) { | ||||||
728 | # a bit more destructive than I'd like... | ||||||
729 | $token_string .= "" . $parser->get_trimmed_text( "/span" ) . ""; | ||||||
730 | $token_string .= ""; | ||||||
731 | $parser->get_tag( "span" ); | ||||||
732 | $token_string .= "99999.99"; | ||||||
733 | $parser->get_trimmed_text; | ||||||
734 | $token_string .= ""; | ||||||
735 | } | ||||||
736 | |||||||
737 | # manage payees | ||||||
738 | if ( $token->[1] eq 'td' and | ||||||
739 | $token->[2]{id} and | ||||||
740 | $token->[2]{id} =~ /payee/i ) { | ||||||
741 | if ( $token->[2]{id} =~ /id109/ ) { | ||||||
742 | $token_string .= "reference $payee_acct"; | ||||||
743 | } elsif ( $token->[2]{id} =~ /id105/ ) { | ||||||
744 | $token_string .= "account_no $payee_acct"; | ||||||
745 | } elsif ( $token->[2]{id} =~ /id113/ ) { | ||||||
746 | $token_string .= "nick $payee_acct"; | ||||||
747 | } elsif ( $token->[2]{id} =~ /id107/) { | ||||||
748 | $token_string .= "nsc $payee_acct"; | ||||||
749 | } elsif ( $token->[2]{id} =~ /id111/) { | ||||||
750 | $token_string .= "currency $payee_acct"; | ||||||
751 | } elsif ( $token->[2]{id} =~ /radiobutton/i ) { | ||||||
752 | $payee_acct++; | ||||||
753 | $token_string .= ""; | ||||||
754 | } | ||||||
755 | $parser->get_tag( "/td" ); | ||||||
756 | $token_string .= " | ";||||||
757 | } | ||||||
758 | |||||||
759 | if ( $token->[1] eq 'select' and | ||||||
760 | ( $self->_streq( $token->[2]{class}, "acc_select" ) or | ||||||
761 | ( $self->_streq( $token->[2]{id}, "form:selectAccountDropDown")))) { | ||||||
762 | my $added_nickname = 0; | ||||||
763 | |||||||
764 | while ( my $account_token = $parser->get_token()) { | ||||||
765 | my $string = $account_token->[0] eq 'T' ? $account_token->[1] : $account_token->[-1]; | ||||||
766 | if ( $account_token->[0] eq 'S' and $account_token->[1] eq 'option' ) { | ||||||
767 | my $val = $account_token->[2]{value}; | ||||||
768 | if ( $val ne 'From Account..' and | ||||||
769 | $val ne 'defaultItem' and | ||||||
770 | $val !~ /^\d+$/ ) { | ||||||
771 | $account_token->[2]{value} = "0"; | ||||||
772 | $string = $self->_rebuild_tag( $account_token ); | ||||||
773 | $val = '0'; | ||||||
774 | } | ||||||
775 | if ( $val ne 'From Account..' and | ||||||
776 | $val ne 'defaultItem' ) { | ||||||
777 | $added_nickname++; | ||||||
778 | if ( $added_nickname == 1 ) { | ||||||
779 | $string .= "Nickname ~ 9999"; | ||||||
780 | } else { | ||||||
781 | $string .= "nick $added_nickname ~ $val"; | ||||||
782 | } | ||||||
783 | } else { | ||||||
784 | $string .= $parser->get_trimmed_text(); | ||||||
785 | } | ||||||
786 | my $tag = $parser->get_tag( "/option" ); | ||||||
787 | if ( $tag ) { | ||||||
788 | $string .= $tag->[-1]; | ||||||
789 | } | ||||||
790 | } | ||||||
791 | $token_string .= $string; | ||||||
792 | last if ( $account_token->[0] eq 'E' and $account_token->[1] eq 'select' ); | ||||||
793 | } | ||||||
794 | } | ||||||
795 | } | ||||||
796 | |||||||
797 | $output .= $token_string; | ||||||
798 | } | ||||||
799 | |||||||
800 | return $output; | ||||||
801 | } | ||||||
802 | |||||||
803 | =back | ||||||
804 | |||||||
805 | =cut | ||||||
806 | |||||||
807 | package Finance::Bank::IE::BankOfIreland::Account; | ||||||
808 | |||||||
809 | # magic (pulled directly from other code, which I now understand) | ||||||
810 | no strict; | ||||||
811 | sub AUTOLOAD { my $self=shift; $AUTOLOAD =~ s/.*:://; $self->{$AUTOLOAD} } | ||||||
812 | |||||||
813 | 1; |