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