File Coverage

blib/lib/Finance/Bank/ID/BCA.pm
Criterion Covered Total %
statement 69 197 35.0
branch 28 92 30.4
condition 11 26 42.3
subroutine 6 19 31.5
pod 5 6 83.3
total 119 340 35.0


line stmt bran cond sub pod time code
1             package Finance::Bank::ID::BCA;
2              
3             our $DATE = '2017-07-20'; # DATE
4             our $VERSION = '0.48'; # VERSION
5              
6 1     1   561238 use 5.010001;
  1         3  
7 1     1   388 use Moo;
  1         5130  
  1         5  
8              
9             extends 'Finance::Bank::ID::Base';
10              
11             has _variant => (is => 'rw'); # bisnis or perorangan
12              
13             sub BUILD {
14 1     1 0 4 my ($self, $args) = @_;
15              
16 1 50       13 $self->site("https://ibank.klikbca.com") unless $self->site;
17 1 50       12 $self->https_host("ibank.klikbca.com") unless $self->https_host;
18             }
19              
20             sub _req {
21 0     0   0 my ($self, @args) = @_;
22              
23             # 2012-03-12 - KlikBCA server since a few week ago rejects TE request
24             # header, so we do not send them.
25 0         0 local @LWP::Protocol::http::EXTRA_SOCK_OPTS =
26             @LWP::Protocol::http::EXTRA_SOCK_OPTS;
27 0         0 push(@LWP::Protocol::http::EXTRA_SOCK_OPTS, SendTE => 0);
28             #$log->tracef("EXTRA_SOCK_OPTS=%s", \@LWP::Protocol::http::EXTRA_SOCK_OPTS);
29              
30 0         0 $self->SUPER::_req(@args);
31             }
32              
33             sub login {
34 0     0 1 0 my ($self) = @_;
35 0         0 my $s = $self->site;
36              
37 0 0       0 return 1 if $self->logged_in;
38 0 0       0 die "400 Username not supplied" unless $self->username;
39 0 0       0 die "400 Password not supplied" unless $self->password;
40              
41 0         0 $self->logger->debug('Logging in ...');
42 0         0 $self->_req(get => [$s], {id=>'login_form'});
43             $self->_req(submit_form => [
44             form_number => 1,
45             fields => {'value(user_id)'=>$self->username,
46             'value(pswd)'=>$self->password,
47             },
48             button => 'value(Submit)',
49             ],
50             {
51             id => 'login',
52             after_request => sub {
53 0     0   0 my ($mech) = @_;
54 0 0       0 $mech->content =~ /var err='(.+?)'/ and return $1;
55 0 0       0 $mech->content =~ /=logout"/ and return;
56 0         0 "unknown login result page";
57             },
58 0         0 });
59 0         0 $self->logged_in(1);
60 0         0 $self->_req(get => ["$s/authentication.do?value(actions)=welcome"],
61             {id=>'welcome'});
62             #$self->_req(get => ["$s/nav_bar_indo/menu_nav.htm"], {id=>'navbar'}); # failed?
63             }
64              
65             sub logout {
66 0     0 1 0 my ($self) = @_;
67              
68 0 0       0 return 1 unless $self->logged_in;
69 0         0 $self->logger->debug('Logging out ...');
70 0         0 $self->_req(get => [$self->site . "/authentication.do?value(actions)=logout"],
71             {id=>'logout'});
72 0         0 $self->logged_in(0);
73             }
74              
75             sub _menu {
76 0     0   0 my ($self) = @_;
77 0         0 my $s = $self->site;
78 0         0 $self->_req(get => ["$s/nav_bar_indo/account_information_menu.htm"],
79             {id=>'accinfo_menu'});
80             }
81              
82             sub list_accounts {
83 0     0 1 0 my ($self) = @_;
84 0         0 $self->login;
85 0         0 $self->logger->info("Listing accounts");
86 0         0 map { $_->{account} } $self->_check_balances;
  0         0  
87             }
88              
89             sub _check_balances {
90 0     0   0 my ($self) = @_;
91 0         0 my $s = $self->site;
92              
93 0         0 my $re = qr!
94             <tr>\s*
95             <td[^>]+>\s*<div[^>]+>\s*<font[^>]+>\s*(\d+)\s*</font>\s*</div>\s*</td>\s*
96             <td[^>]+>\s*<div[^>]+>\s*<font[^>]+>\s*([^<]*?)\s*</font>\s*</div>\s*</td>\s*
97             <td[^>]+>\s*<div[^>]+>\s*<font[^>]+>\s*([A-Z]+)\s*</font>\s*</div>\s*</td>\s*
98             <td[^>]+>\s*<div[^>]+>\s*<font[^>]+>\s*([0-9,.]+)\.(\d\d)\s*</font>\s*</div>\s*</td>
99             !x;
100              
101 0         0 $self->login;
102 0         0 $self->_menu;
103             $self->_req(post => ["$s/balanceinquiry.do"],
104             {
105             id => 'check_balance',
106             after_request => sub {
107 0     0   0 my ($mech) = @_;
108 0         0 my $errmsg = $self->_get_bca_errmsg;
109 0 0       0 return "BCA errmsg: $errmsg" if $errmsg;
110 0 0       0 $mech->content =~ $re or
111             return "can't find balances, maybe page layout changed?";
112 0         0 '';
113             },
114 0         0 });
115              
116 0         0 my @res;
117 0         0 my $content = $self->mech->content;
118 0         0 while ($content =~ m/$re/og) {
119 0         0 push @res, { account => $1,
120             account_type => $2,
121             currency => $3,
122             balance => $self->_stripD($4) + 0.01*$5,
123             };
124             }
125 0         0 @res;
126             }
127              
128             # parse error message from error page, often shown when we want to check
129             # statement or balance.
130             sub _get_bca_errmsg {
131 0     0   0 my $self = shift;
132 0         0 my $mech = $self->mech;
133 0         0 my $ct = $mech->content;
134 0 0       0 return $1 if $ct =~ m!^<font.+?red><b>(.+)</b></font>!m;
135             }
136              
137             sub check_balance {
138 0     0 1 0 my ($self, $account) = @_;
139 0         0 my @bals = $self->_check_balances;
140 0 0       0 return unless @bals;
141 0 0       0 return $bals[0]{balance} if !$account;
142 0         0 for (@bals) {
143 0 0       0 return $_->{balance} if $_->{account} eq $account;
144             }
145 0         0 return;
146             }
147              
148             sub get_statement {
149 0     0 1 0 require DateTime;
150              
151 0         0 my ($self, %args) = @_;
152 0         0 my $s = $self->site;
153 0         0 my $max_days = 31;
154              
155 0         0 $self->login;
156 0         0 $self->_menu;
157             $self->logger->info("Getting statement for ".
158 0 0       0 ($args{account} ? "account `$args{account}'" : "default account")." ...");
159             $self->_req(post => ["$s/accountstmt.do?value(actions)=acct_stmt"],
160             {
161             id => 'get_statement_form',
162             after_request => sub {
163 0     0   0 my ($mech) = @_;
164 0         0 my $errmsg = $self->_get_bca_errmsg;
165 0 0       0 return "BCA errmsg: $errmsg" if $errmsg;
166 0 0       0 $mech->content =~ /<form/i or
167             return "no form found, maybe we got logged out?";
168 0         0 '';
169             },
170 0         0 });
171              
172 0         0 my $form = $self->mech->form_number(1);
173              
174             # in the site this is done by javascript onSubmit(), so we emulate it here
175 0         0 $form->action("$s/accountstmt.do?value(actions)=acctstmtview");
176              
177             # in the case of the current date being a saturday/sunday/holiday, end
178             # date will be forwarded 1 or more days from the current date by the site,
179             # so we need to know end date and optionally forward start date when needed,
180             # to avoid total number of days being > 31.
181              
182 0         0 my $today = DateTime->today;
183 0         0 my $max_dt = DateTime->new(day => $form->value("value(endDt)"),
184             month => $form->value("value(endMt)"),
185             year => $form->value("value(endYr)"));
186 0         0 my $cmp = DateTime->compare($today, $max_dt);
187 0         0 my $delta_days = $cmp * $today->subtract_datetime($max_dt, $today)->days;
188 0 0       0 if ($delta_days > 0) {
189 0         0 $self->logger->warn("Something weird is going on, end date is being ".
190             "set less than today's date by the site (".
191             $self->_fmtdate($max_dt)."). ".
192             "Please check your computer's date setting. ".
193             "Continuing anyway.");
194             }
195 0         0 my $min_dt = $max_dt->clone->subtract(days => ($max_days-1));
196              
197 0   0     0 my $end_dt = $args{end_date} || $max_dt;
198             my $start_dt = $args{start_date} ||
199 0   0     0 $end_dt->clone->subtract(days => (($args{days} || $max_days)-1));
200 0 0       0 if (DateTime->compare($start_dt, $min_dt) == -1) {
201 0         0 $self->logger->warn("Start date ".$self->_fmtdate($start_dt)." is less than ".
202             "minimum date ".$self->_fmtdate($min_dt).". Setting to ".
203             "minimum date instead.");
204 0         0 $start_dt = $min_dt;
205             }
206 0 0       0 if (DateTime->compare($start_dt, $max_dt) == 1) {
207 0         0 $self->logger->warn("Start date ".$self->_fmtdate($start_dt)." is greater than ".
208             "maximum date ".$self->_fmtdate($max_dt).". Setting to ".
209             "maximum date instead.");
210 0         0 $start_dt = $max_dt;
211             }
212 0 0       0 if (DateTime->compare($end_dt, $min_dt) == -1) {
213 0         0 $self->logger->warn("End date ".$self->_fmtdate($end_dt)." is less than ".
214             "minimum date ".$self->_fmtdate($min_dt).". Setting to ".
215             "minimum date instead.");
216 0         0 $end_dt = $min_dt;
217             }
218 0 0       0 if (DateTime->compare($end_dt, $max_dt) == 1) {
219 0         0 $self->logger->warn("End date ".$self->_fmtdate($end_dt)." is greater than ".
220             "maximum date ".$self->_fmtdate($max_dt).". Setting to ".
221             "maximum date instead.");
222 0         0 $end_dt = $max_dt;
223             }
224 0 0       0 if (DateTime->compare($start_dt, $end_dt) == 1) {
225 0         0 $self->logger->warn("Start date ".$self->_fmtdate($start_dt)." is greater than ".
226             "end date ".$self->_fmtdate($end_dt).". Setting to ".
227             "end date instead.");
228 0         0 $start_dt = $end_dt;
229             }
230              
231 0         0 my $select = $form->find_input("value(D1)");
232 0         0 my $d1 = $select->value;
233 0 0       0 if ($args{account}) {
234 0         0 my @d1 = $select->possible_values;
235 0         0 my @accts = $select->value_names;
236 0         0 for (0..$#accts) {
237 0 0       0 if ($args{account} eq $accts[$_]) {
238 0         0 $d1 = $d1[$_];
239 0         0 last;
240             }
241             }
242             }
243              
244             $self->_req(submit_form => [
245             form_number => 1,
246             fields => {
247             "value(D1)" => $d1,
248             "value(startDt)" => $start_dt->day,
249             "value(startMt)" => $start_dt->month,
250             "value(startYr)" => $start_dt->year,
251             "value(endDt)" => $end_dt->day,
252             "value(endMt)" => $end_dt->month,
253             "value(endYr)" => $end_dt->year,
254             },
255             ],
256             {
257             id => 'get_statement',
258             after_request => sub {
259 0     0   0 my ($mech) = @_;
260 0         0 my $errmsg = $self->_get_bca_errmsg;
261 0 0       0 return "BCA errmsg: $errmsg" if $errmsg;
262 0         0 '';
263             },
264 0         0 });
265 0   0     0 my $parse_opts = $args{parse_opts} // {};
266 0         0 my $resp = $self->parse_statement($self->mech->content, %$parse_opts);
267 0 0 0     0 return if !$resp || $resp->[0] != 200;
268 0         0 $resp->[2];
269             }
270              
271             sub _ps_detect {
272 7     7   19 my ($self, $page) = @_;
273 7 50       676 unless ($page =~ /(?:^\s*|&nbsp;)(?:INFORMASI REKENING - MUTASI REKENING|ACCOUNT INFORMATION - ACCOUNT STATEMENT)/mi) {
274 0         0 return "No KlikBCA statement page signature found";
275             }
276 7 100       171 $self->_variant($page =~ /^(?:Kode Mata Uang|Currency)/m ? 'bisnis' : 'perorangan');
277 7         35 "";
278             }
279              
280             sub _ps_get_metadata {
281 7     7   73 require DateTime;
282              
283 7         36 my ($self, $page, $stmt) = @_;
284              
285 7 50       1092 unless ($page =~ /\s*(?:(?:Nomor|No\.) [Rr]ekening|Account Number)\s*(?:<[^>]+>\s*)*[:\t]\s*(?:<[^>]+>\s*)*([\d-]+)/m) {
286 0         0 return "can't get account number";
287             }
288 7         51 $stmt->{account} = $self->_stripD($1);
289 7         19 $stmt->{account} =~ s/\D+//g;
290              
291 7         16 my $adv1 = "probably the statement format changed, or input incomplete";
292              
293 7 50       796 unless ($page =~ m!(?:^\s*|>)(?:Periode|Period)\s*(?:<[^>]+>\s*)*[:\t]\s*(?:<[^>]+>\s*)*(\d\d)/(\d\d)/(\d\d\d\d) - (\d\d)/(\d\d)/(\d\d\d\d)!m) {
294 0         0 return "can't get statement period, $adv1";
295             }
296 7         62 $stmt->{start_date} = DateTime->new(day=>$1, month=>$2, year=>$3);
297 7         2424 $stmt->{end_date} = DateTime->new(day=>$4, month=>$5, year=>$6);
298              
299 7 50       2872 unless ($page =~ /(?:^|>)(?:(?:Kode )?Mata Uang|Currency)\s*(?:<[^>]+>\s*)*[:\t]\s*(?:<[^>]+>\s*)*(Rp|[A-Z]+)/m) {
300 0         0 return "can't get currency, $adv1";
301             }
302 7 100       40 $stmt->{currency} = ($1 eq 'Rp' ? 'IDR' : $1);
303              
304 7 50       65 unless ($page =~ /(?:^|>)(?:Nama|Name)\s*(?:<[^>]+>\s*)*[:\t]\s*(?:<[^>]+>\s*)*([^<\015\012]+)/m) {
305 0         0 return "can't get account holder, $adv1";
306             }
307 7         22 $stmt->{account_holder} = $1;
308              
309 7 50       3318 unless ($page =~ /(?:^|>)(?:Mutasi Kredit|Total Credits)\s*(?:<[^>]+>\s*)*[:\t]\s*(?:<[^>]+>\s*)*([0-9,.]+)\.(\d\d)(?:\s*\t\s*(\d+))?/m) {
310 0         0 return "can't get total credit, $adv1";
311             }
312 7         33 $stmt->{_total_credit_in_stmt} = $self->_stripD($1) + 0.01*$2;
313 7 100       25 $stmt->{_num_credit_tx_in_stmt} = $3 if $3;
314              
315 7 50       3513 unless ($page =~ /(?:^|>)(?:Mutasi Debet|Total Debits)\s*(?:<[^>]+>\s*)*[:\t]\s*(?:<[^>]+>\s*)*([0-9,.]+)\.(\d\d)(?:\s*\t\s*(\d+))?/m) {
316 0         0 return "can't get total credit, $adv1";
317             }
318 7         26 $stmt->{_total_debit_in_stmt} = $self->_stripD($1) + 0.01*$2;
319 7 100       25 $stmt->{_num_debit_tx_in_stmt} = $3 if $3;
320 7         35 "";
321             }
322              
323             sub _ps_get_transactions {
324 7     7   38 require DateTime;
325              
326 7         17 my ($self, $page, $stmt) = @_;
327              
328 7         16 my @e;
329             # text version
330 7         570 while ($page =~ m!^
331             (\d\d/\d\d|\s?PEND|\s?NEXT) # 1) date
332             (?:\s*\t\s*|\n)+
333             ((?:[^\t]|\n)*?) # 2) description
334             (?:\s*\t\s*|\n)+
335             (\d{4}) # 3) branch code
336             (?:\s*\t\s*|\n)+
337             ([0-9,]+)\.(\d\d) # 4+5) amount
338             (?:\s*\t?\s*|\n)+
339             (CR|DB) # 6)
340             (?:\s*\t\s*|\n)+
341             ([0-9,]+)\.(\d\d) # 7+8) balance
342             !mxg) {
343 59         1006 push @e, {date=>$1, desc=>$2, br=>$3, amt=>$4, amtf=>$5, crdb=>$6, bal=>$7, balf=>$8};
344             }
345 7 100       29 if (!@e) {
346             # HTML version
347 2         1598 while ($page =~ m!^
348             <tr>\s*
349             <td[^>]+>(?:\s*<[^>]+>\s*)* (\d\d/\d\d|\s?PEND|\s?NEXT) (?:\s*<[^>]+>\s*)*</td>\s*
350             <td[^>]+>(?:\s*<[^>]+>\s*)* ((?:[^\t]|\n)*?) (?:\s*<[^>]+>\s*)*</td>\s*
351             <td[^>]+>(?:\s*<[^>]+>\s*)* (\d{4}) (?:\s*<[^>]+>\s*)*</td>\s*
352             <td[^>]+>(?:\s*<[^>]+>\s*)* ([0-9,]+)\.(\d\d) (?:\s*<[^>]+>\s*)*</td>\s*
353             <td[^>]+>(?:\s*<[^>]+>\s*)* (CR|DB) (?:\s*<[^>]+>\s*)*</td>\s*
354             <td[^>]+>(?:\s*<[^>]+>\s*)* ([0-9,]+)\.(\d\d) (?:\s*<[^>]+>\s*)*</td>\s*
355             </tr>
356             !smxg) {
357 34         8809 push @e, {date=>$1, desc=>$2, br=>$3, amt=>$4, amtf=>$5, crdb=>$6, bal=>$7, balf=>$8};
358             }
359 2         7 for (@e) { $_->{desc} =~ s!<br ?/?>!\n!ig }
  34         132  
360             }
361              
362 7         31 my @tx;
363             my @skipped_tx;
364 7         0 my $last_date;
365 7         0 my $seq;
366 7         13 my $i = 0;
367 7         21 for my $e (@e) {
368 93         142 $i++;
369 93         163 my $tx = {};
370             #$tx->{stmt_start_date} = $stmt->{start_date};
371              
372 93 50       329 if ($e->{date} =~ /NEXT/) {
    50          
373 0         0 $tx->{date} = $stmt->{end_date};
374 0         0 $tx->{is_next} = 1;
375             } elsif ($e->{date} =~ /PEND/) {
376 0         0 $tx->{date} = $stmt->{end_date};
377 0         0 $tx->{is_pending} = 1;
378             } else {
379 93         328 my ($day, $mon) = split m!/!, $e->{date};
380             my $last_nonpend_date = DateTime->new(
381             year => ($mon < $stmt->{start_date}->month ?
382             $stmt->{end_date}->year :
383 93 50       295 $stmt->{start_date}->year),
384             month => $mon,
385             day => $day);
386 93         26009 $tx->{date} = $last_nonpend_date;
387 93         194 $tx->{is_pending} = 0;
388             }
389              
390 93         197 $tx->{description} = $e->{desc};
391              
392 93         181 $tx->{branch} = $e->{br};
393              
394 93 100       423 $tx->{amount} = ($e->{crdb} =~ /CR/ ? 1 : -1) * ($self->_stripD($e->{amt}) + 0.01*$e->{amtf});
395 93         262 $tx->{balance} = ($self->_stripD($e->{bal}) + 0.01*$e->{balf});
396              
397 93 100 100     320 if (!$last_date || DateTime->compare($last_date, $tx->{date})) {
398 33         1865 $seq = 1;
399 33         62 $last_date = $tx->{date};
400             } else {
401 60         4712 $seq++;
402             }
403 93         224 $tx->{seq} = $seq;
404              
405 93 50 100     427 if ($self->_variant eq 'perorangan' &&
      66        
406             $tx->{date}->dow =~ /6|7/ &&
407             $tx->{description} !~ /^(BIAYA ADM|BUNGA|(CR|DR) KOREKSI BUNGA|PAJAK BUNGA)\s*$/) {
408 0         0 return "check failed in tx#$i: In KlikBCA Perorangan, all ".
409             "transactions must not be in Sat/Sun except for Interest and ".
410             "Admin Fee: <$tx->{description}> ($tx->{date})";
411             # note: in Tahapan perorangan, BIAYA ADM is set on
412             # Fridays, but for Tapres (?) on last day of the month
413             }
414              
415 93 50 66     817 if ($self->_variant eq 'bisnis' &&
      33        
416             $tx->{date}->dow =~ /6|7/ &&
417             $tx->{description} !~ /^(BIAYA ADM|BUNGA|(CR|DR) KOREKSI BUNGA|PAJAK BUNGA)\s*$/) {
418 0         0 return "check failed in tx#$i: In KlikBCA Bisnis, all ".
419             "transactions must not be in Sat/Sun except for Interest and ".
420             "Admin Fee: <$tx->{description}> ($tx->{date})";
421             # note: in KlikBCA bisnis, BIAYA ADM is set on the last day of the
422             # month, regardless of whether it's Sat/Sun or not
423             }
424              
425 93         332 push @tx, $tx;
426             }
427 7         25 $stmt->{transactions} = \@tx;
428 7         19 $stmt->{skipped_transactions} = \@skipped_tx;
429 7         114 "";
430             }
431              
432             1;
433             # ABSTRACT: Check your BCA accounts from Perl
434              
435             __END__
436              
437             =pod
438              
439             =encoding UTF-8
440              
441             =head1 NAME
442              
443             Finance::Bank::ID::BCA - Check your BCA accounts from Perl
444              
445             =head1 VERSION
446              
447             This document describes version 0.48 of Finance::Bank::ID::BCA (from Perl distribution Finance-Bank-ID-BCA), released on 2017-07-20.
448              
449             =head1 SYNOPSIS
450              
451             If you just want to download banking statements, and you use Linux/Unix, you
452             might want to use the L<download-bca> script instead of having to deal with this
453             library directly.
454              
455             If you want to use the library in your Perl application:
456              
457             use Finance::Bank::ID::BCA;
458              
459             # FBI::BCA uses Log::ger. to show logs to, for example, screen:
460             use Log::ger::Output 'Screen';
461              
462             my $ibank = Finance::Bank::ID::BCA->new(
463             username => 'ABCDEFGH1234', # opt if only using parse_statement()
464             password => '123456', # idem
465             verify_https => 1, # default is 0
466             #https_ca_dir => '/etc/ssl/certs', # default is already /etc/ssl/certs
467             );
468              
469             eval {
470             $ibank->login(); # dies on error
471              
472             my @accts = $ibank->list_accounts();
473              
474             my $bal = $ibank->check_balance($acct); # $acct is optional
475              
476             my $stmt = $ibank->get_statement(
477             account => ..., # opt, default account will be used if undef
478             days => 31, # opt
479             start_date => DateTime->new(year=>2009, month=>10, day=>6),
480             # opt, takes precedence over 'days'
481             end_date => DateTime->today, # opt, takes precedence over 'days'
482             );
483              
484             print "Transactions: ";
485             for my $tx (@{ $stmt->{transactions} }) {
486             print "$tx->{date} $tx->{amount} $tx->{description}\n";
487             }
488             };
489             warn if $@;
490              
491             # remember to call this, otherwise you will have trouble logging in again
492             # for some time
493             $ibank->logout();
494              
495             Utility routines:
496              
497             # parse HTML statement directly
498             my $res = $ibank->parse_statement($html);
499              
500             =head1 DESCRIPTION
501              
502             This module provide a rudimentary interface to the web-based online banking
503             interface of the Indonesian B<Bank Central Asia> (BCA) at
504             https://ibank.klikbca.com. You will need either L<Crypt::SSLeay> or
505             L<IO::Socket::SSL> installed for HTTPS support to work (and strictly
506             Crypt::SSLeay to enable certificate verification). L<WWW::Mechanize> is required
507             but you can supply your own mech-like object.
508              
509             This module can only login to the retail/personal version of the site (KlikBCA
510             perorangan) and not the corporate/business version (KlikBCA bisnis) as the later
511             requires VPN and token input on login. But this module can parse statement page
512             from both versions.
513              
514             Warning: This module is neither offical nor is it tested to be 100% safe!
515             Because of the nature of web-robots, everything may break from one day to the
516             other when the underlying web interface changes.
517              
518             =head1 WARNING
519              
520             This warning is from Simon Cozens' C<Finance::Bank::LloydsTSB>, and seems just
521             as apt here.
522              
523             This is code for B<online banking>, and that means B<your money>, and that means
524             B<BE CAREFUL>. You are encouraged, nay, expected, to audit the source of this
525             module yourself to reassure yourself that I am not doing anything untoward with
526             your banking data. This software is useful to me, but is provided under B<NO
527             GUARANTEE>, explicit or implied.
528              
529             =head1 ERROR HANDLING AND DEBUGGING
530              
531             Most methods die() when encountering errors, so you can use eval() to trap them.
532              
533             Full response headers and bodies are dumped to a separate logger. See
534             documentation on C<new()> below and the sample script in examples/ subdirectory
535             in the distribution.
536              
537             =head1 ATTRIBUTES
538              
539             =head1 METHODS
540              
541             =for Pod::Coverage BUILD
542              
543             =head2 new(%args)
544              
545             Create a new instance. %args keys:
546              
547             =over 4
548              
549             =item * username
550              
551             Optional if you are just using utility methods like C<parse_statement()> and not
552             C<login()> etc.
553              
554             =item * password
555              
556             Optional if you are just using utility methods like C<parse_statement()> and not
557             C<login()> etc.
558              
559             =item * mech
560              
561             Optional. A L<WWW::Mechanize>-like object. By default this module instantiate a
562             new L<Finance::BankUtils::ID::Mechanize> (a WWW::Mechanize subclass) object to
563             retrieve web pages, but if you want to use a custom/different one, you are
564             allowed to do so here. Use cases include: you want to retry and increase timeout
565             due to slow/unreliable network connection (using
566             L<WWW::Mechanize::Plugin::Retry>), you want to slow things down using
567             L<WWW::Mechanize::Sleepy>, you want to use IE engine using
568             L<Win32::IE::Mechanize>, etc.
569              
570             =item * verify_https
571              
572             Optional. If you are using the default mech object (see previous option), you
573             can set this option to 1 to enable SSL certificate verification (recommended for
574             security). Default is 0.
575              
576             SSL verification will require a CA bundle directory, default is /etc/ssl/certs.
577             Adjust B<https_ca_dir> option if your CA bundle is not located in that
578             directory.
579              
580             =item * https_ca_dir
581              
582             Optional. Default is /etc/ssl/certs. Used to set HTTPS_CA_DIR environment
583             variable for enabling certificate checking in Crypt::SSLeay. Only used if
584             B<verify_https> is on.
585              
586             =item * logger
587              
588             Optional. You can supply any object that responds to trace(), debug(), info(),
589             warn(), error(), or fatal() here. If not specified, this module will use a
590             default logger.
591              
592             =item * logger_dump
593              
594             Optional. This is just like C<logger> but this module will log contents of
595             response here instead of to C<logger> for debugging purposes. You can configure
596             something like L<Log::ger::Output::DirWriteRotate> to save web pages more
597             conveniently as separate files. If unspecified, the default logger is used (same
598             as C<logger>).
599              
600             =back
601              
602             =head2 login()
603              
604             Login to the net banking site. You actually do not have to do this explicitly as
605             login() is called by other methods like C<check_balance()> or
606             C<get_statement()>.
607              
608             If login is successful, C<logged_in> will be set to true and subsequent calls to
609             C<login()> will become a no-op until C<logout()> is called.
610              
611             Dies on failure.
612              
613             =head2 logout()
614              
615             Logout from the net banking site. You need to call this at the end of your
616             program, otherwise the site will prevent you from re-logging in for some time
617             (e.g. 10 minutes).
618              
619             If logout is successful, C<logged_in> will be set to false and subsequent calls
620             to C<logout()> will become a no-op until C<login()> is called.
621              
622             Dies on failure.
623              
624             =head2 list_accounts()
625              
626             Return an array containing all account numbers that are associated with the
627             current net banking login.
628              
629             =head2 check_balance([$account])
630              
631             Return balance for specified account, or the default account if C<$account> is
632             not specified.
633              
634             =head2 get_statement(%args) => $stmt
635              
636             Get account statement. %args keys:
637              
638             =over 4
639              
640             =item * account
641              
642             Optional. Select the account to get statement of. If not specified, will use the
643             already selected account.
644              
645             =item * days
646              
647             Optional. Number of days between 1 and 31. If days is 1, then start date and end
648             date will be the same. Default is 31.
649              
650             =item * start_date
651              
652             Optional. Default is end_date - days.
653              
654             =item * end_date
655              
656             Optional. Default is today (or some 1+ days from today if today is a
657             Saturday/Sunday/holiday, depending on the default value set by the site's form).
658              
659             =back
660              
661             See parse_statement() on structure of $stmt.
662              
663             =head2 parse_statement($html, %opts) => $res
664              
665             Given the HTML text of the account statement results page, parse it into
666             structured data:
667              
668             $stmt = {
669             start_date => $start_dt, # a DateTime object
670             end_date => $end_dt, # a DateTime object
671             account_holder => STRING,
672             account => STRING, # account number
673             currency => STRING, # 3-digit currency code
674             transactions => [
675             # first transaction
676             {
677             date => $dt, # a DateTime obj, book date ("tanggal pembukuan")
678             seq => INT, # a number >= 1 which marks the sequence of
679             # transactions for the day
680             amount => REAL, # a real number, positive means credit (deposit),
681             # negative means debit (withdrawal)
682             description => STRING,
683             is_pending => BOOL,
684             branch => STRING, # a 4-digit branch/ATM code
685             balance => REAL,
686             },
687             # second transaction
688             ...
689             ]
690             }
691              
692             Returns:
693              
694             [$status, $err_details, $stmt]
695              
696             C<$status> is 200 if successful or some other 3-digit code if parsing failed.
697             C<$stmt> is the result (structure as above, or undef if parsing failed).
698              
699             Options:
700              
701             =over 4
702              
703             =item * return_datetime_obj => BOOL
704              
705             Default is true. If set to false, the method will return dates as strings with
706             this format: 'YYYY-MM-DD HH::mm::SS' (produced by DateTime->dmy . ' ' .
707             DateTime->hms). This is to make it easy to pass the data structure into YAML,
708             JSON, MySQL, etc. Nevertheless, internally DateTime objects are still used.
709              
710             =back
711              
712             Additional notes:
713              
714             The method can also handle some copy-pasted text from the GUI browser, but this
715             is no longer documented or guaranteed to keep working.
716              
717             =head1 HOMEPAGE
718              
719             Please visit the project's homepage at L<https://metacpan.org/release/Finance-Bank-ID-BCA>.
720              
721             =head1 SOURCE
722              
723             Source repository is at L<https://github.com/perlancar/perl-Finance-Bank-ID-BCA>.
724              
725             =head1 BUGS
726              
727             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Finance-Bank-ID-BCA>
728              
729             When submitting a bug or request, please include a test-file or a
730             patch to an existing test-file that illustrates the bug or desired
731             feature.
732              
733             =head1 AUTHOR
734              
735             perlancar <perlancar@cpan.org>
736              
737             =head1 COPYRIGHT AND LICENSE
738              
739             This software is copyright (c) 2017, 2015, 2014, 2013, 2012, 2011, 2010 by perlancar@cpan.org.
740              
741             This is free software; you can redistribute it and/or modify it under
742             the same terms as the Perl 5 programming language system itself.
743              
744             =cut