File Coverage

blib/lib/Finance/Bank/ID/BPRKS.pm
Criterion Covered Total %
statement 74 201 36.8
branch 21 90 23.3
condition 2 14 14.2
subroutine 12 24 50.0
pod 5 6 83.3
total 114 335 34.0


\s* \s* \s* \s* \s*]*>(\d+)!s) { \s*!s) { \s*]*>(\w+)!s) { \s*]*>([^<]+?)\s*!s) { \s*]*>([^<]+)!s) { \s*]*>([^<]+)!s) { ]*>\s* \s* \s* \s* \s* \s* !sxg) {
line stmt bran cond sub pod time code
1             package Finance::Bank::ID::BPRKS;
2              
3             our $DATE = '2015-08-17'; # DATE
4             our $VERSION = '0.04'; # VERSION
5              
6 1     1   217714 use 5.010001;
  1         3  
7 1     1   813 use Moo;
  1         30317  
  1         7  
8 1     1   1512 use DateTime;
  1         3  
  1         22  
9 1     1   1133 use Log::Any::IfLOG '$log';
  1         11  
  1         5  
10              
11 1     1   720 use Parse::Number::ID qw(parse_number_id);
  1         616  
  1         2307  
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 4262 my ($self, $args) = @_;
21              
22 1 50       20 $self->site("https://ib.bprks.co.id") unless $self->site;
23 1 50       27 $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   7 my $self = shift;
43 4         33 local $_ = lc(shift);
44 4 50       61 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         25 return 5;
54             } elsif (/^(?:jun|jun[eiy])$/) {
55 2         13 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   23 my ($self, $s) = @_;
76 12         32 my $neg = $s =~ s/^\((.+)\)$/$1/;
77 12         37 my $n = parse_number_id(text => $s);
78 12 100       474 $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            
141             ]+>\s*]+>\s*]+>\s*(\d+)\s*\s*\s*
142             ]+>\s*]+>\s*]+>\s*([^<]*?)\s*\s*\s*
143             ]+>\s*]+>\s*]+>\s*([A-Z]+)\s*\s*\s*
144             ]+>\s*]+>\s*]+>\s*([0-9,.]+)\.(\d\d)\s*\s*\s*
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 =~ /
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   26458 my ($self, $page) = @_;
296 2 50       30 unless ($page =~ />Detail Informasi Mutasi Rekening
297 0         0 return "No BPR KS statement page signature found";
298             }
299 2         15 $self->_variant('individual');
300 2         6 "";
301             }
302              
303             sub _ps_get_metadata {
304 2     2   15 my ($self, $page, $stmt) = @_;
305              
306 2 50       95 unless ($page =~ m!]*>No\. Rekening
307 0         0 return "can't get account number";
308             }
309 2         14 $stmt->{account} = $1;
310              
311 2         5 my $adv1 = "probably the statement format changed, or input incomplete";
312              
313 2 50       64 unless ($page =~ m!]*>Periode Mutasi Rekening\s*(?\d{1,2})-(?\w+)-(?\d{4})\s*s/d\s*(?\d{1,2})-(?\w+)-(?\d{4})\s*
314 0         0 return "can't get statement period, $adv1";
315             }
316 1     1   831 $stmt->{start_date} = DateTime->new(day=>$+{d1}, month=>$self->_parse_mon($+{m1}), year=>$+{y1});
  1         470  
  1         689  
  2         20  
317 2         675 $stmt->{end_date} = DateTime->new(day=>$+{d2}, month=>$self->_parse_mon($+{m2}), year=>$+{y2});
318              
319 2 50       464 unless ($page =~ m!]*>Mata Uang
320 0         0 return "can't get currency, $adv1";
321             }
322 2 50       14 $stmt->{currency} = ($1 eq 'Rp' ? 'IDR' : $1);
323              
324 2 50       52 unless ($page =~ m!]*>Nama
325 0         0 return "can't get account holder, $adv1";
326             }
327 2         6 $stmt->{account_holder} = $1;
328              
329             # additional: Tipe: Tabungan
330              
331 2 50       67 unless ($page =~ m!]*>Mutasi Kredit
332 0         0 return "can't get total credit, $adv1";
333             }
334 2         10 $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       65 unless ($page =~ m!]*>Mutasi Debet
338 0         0 return "can't get total credit, $adv1";
339             }
340 2         7 $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         7 "";
343             }
344              
345             sub _ps_get_transactions {
346 2     2   13 my ($self, $page, $stmt) = @_;
347              
348 2         4 my @e;
349 2         112 while ($page =~ m!
350            
351             ]+>\s* (?\d\d/\d\d/\d\d\d\d) \s*
352             ]+>\s* (?[^<]+?) \s*
353             ]+>\s* (?[^<]+?) \s*
354             ]+>\s* (?[^<]+?) \s*
355             ]+>\s* (?[^<]+?) \s*
356            
357 4         158 my %m = %+;
358 4         53 push @e, \%m;
359             }
360              
361 2         4 my @tx;
362             my @skipped_tx;
363 0         0 my $last_date;
364 0         0 my $seq;
365 2         2 my $i = 0;
366 2         6 for my $e (@e) {
367 4         6 $i++;
368 4         6 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         16 my ($day, $mon, $year) = split m!/!, $e->{date};
380 4         17 my $last_nonpend_date = DateTime->new(
381             year => $year,
382             month => $mon,
383             day => $day);
384 4         765 $tx->{date} = $last_nonpend_date;
385             ###$tx->{is_pending} = 0;
386             ###}
387              
388 4         11 $tx->{description} = $e->{desc};
389              
390 4         10 $tx->{amount} = $self->_parse_num($e->{amt});
391 4         12 $tx->{balance} = $self->_parse_num($e->{bal});
392              
393             ### bca
394             ###if ($tx->{is_next} && $self->skip_NEXT) {
395             ###}
396              
397 4 100 66     22 if (!$last_date || DateTime->compare($last_date, $tx->{date})) {
398 2         2 $seq = 1;
399 2         5 $last_date = $tx->{date};
400             } else {
401 2         273 $seq++;
402             }
403 4         9 $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         13 push @tx, $tx;
431             ###}
432             }
433 2         6 $stmt->{transactions} = \@tx;
434 2         5 $stmt->{skipped_transactions} = \@skipped_tx;
435 2         13 "";
436             }
437              
438             1;
439             # ABSTRACT: Check your BPR KS accounts from Perl
440              
441             __END__