├── src └── IBank │ ├── Log.php │ ├── Modules │ ├── BCA │ │ ├── README.md │ │ ├── BCA-target.yml │ │ ├── BCA-reference.yml │ │ ├── BCA-menu.yml │ │ └── BCA.php │ ├── BNI │ │ ├── README.md │ │ ├── BNI-reference.yml │ │ ├── BNI-target.yml │ │ ├── BNI-menu.yml │ │ └── BNI.php │ └── Mandiri │ │ ├── Mandiri-target.yml │ │ ├── README.md │ │ ├── Mandiri-reference.yml │ │ ├── Mandiri-menu.yml │ │ └── Mandiri.php │ ├── WebCrawlerModuleTrait.php │ └── IBank.php ├── .gitignore ├── README.md ├── demo ├── README.md ├── bca-logout.php ├── bca-get-balance.php ├── bni-get-balance.php ├── bca-get-transaction.php └── bni-get-transaction.php ├── composer.json ├── misc └── BNI-http-response-body-maintenance.html └── LICENSE /src/IBank/Log.php: -------------------------------------------------------------------------------- 1 | Tanggal Akhir tidak boleh melebihi tanggal hari ini 16 | 17 | Padahal tanggal akhir sama dengan hari ini, kemungkinan disebabkan pukul 00:00 18 | sd 01:00 masih belum dianggap hari baru oleh server. 19 | -------------------------------------------------------------------------------- /src/IBank/Modules/BCA/BCA-target.yml: -------------------------------------------------------------------------------- 1 | # Target Definitions. 2 | target: 3 | get_balance: 4 | - handler: visit 5 | handler_before: bca_check_session 6 | menu: bca_balance_inquiry_page 7 | visit_before: 8 | - bca_set_referer 9 | - bca_method_post 10 | visit_after: verify 11 | get_transaction: 12 | - handler: visit 13 | handler_before: bca_check_session 14 | menu: bca_account_statement_page 15 | visit_before: 16 | - bca_set_referer 17 | - bca_method_post 18 | visit_after: verify 19 | - handler: visit 20 | menu: bca_account_statement_page_view 21 | visit_after: verify 22 | logout: 23 | - handler: visit 24 | menu: bca_logout 25 | visit_before: 26 | - bca_set_referer 27 | - bca_method_post 28 | - handler: bca_clear_last_visit 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ijortengab/ibank", 3 | "description": "Internet Banking Helper for grab information of your internet banking account.", 4 | "type": "project", 5 | "license": "GPL", 6 | "authors": [ 7 | { 8 | "name": "IjorTengab", 9 | "email": "m_roji28@yahoo.com", 10 | "homepage": "http://github.com/ijortengab" 11 | } 12 | ], 13 | "require": { 14 | "fabpot/goutte": "^4.0", 15 | "symfony/console": "^5.2", 16 | "symfony/workflow": "^5.2", 17 | "symfony/yaml": "^5.2", 18 | "symfony/dom-crawler": "^5.2" 19 | }, 20 | "autoload": { 21 | "psr-4": {"IjorTengab\\IBank\\": "src/IBank/"} 22 | }, 23 | 24 | "repositories": [ 25 | { 26 | "type": "vcs", 27 | "url": "https://github.com/ijortengab/logger" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/IBank/Modules/Mandiri/Mandiri-target.yml: -------------------------------------------------------------------------------- 1 | # Target Definitions. 2 | target: 3 | get_balance: 4 | - handler: visit 5 | # handler_before: bca_check_session 6 | menu: mandiri_home_page 7 | # visit_before: 8 | # - bca_set_referer 9 | # - bca_method_post 10 | visit_after: verify 11 | - handler: visit 12 | menu : mandiri_login_form 13 | # get_transaction: 14 | # - handler: visit 15 | # handler_before: bca_check_session 16 | # menu: bca_account_statement_page 17 | # visit_before: 18 | # - bca_set_referer 19 | # - bca_method_post 20 | # visit_after: verify 21 | # - handler: visit 22 | # menu: bca_account_statement_page_view 23 | # visit_after: verify 24 | # logout: 25 | # - handler: visit 26 | # menu: bca_logout 27 | # visit_before: 28 | # - bca_set_referer 29 | # - bca_method_post 30 | # - handler: bca_clear_last_visit 31 | -------------------------------------------------------------------------------- /demo/bca-logout.php: -------------------------------------------------------------------------------- 1 | '; 17 | 18 | // 4. Execute. 19 | $result = IBank::BCA('logout', $information); 20 | echo '$result: ', print_r($result, true), PHP_EOL; 21 | 22 | // 5. Disarankan untuk mengecek log (error/notice/debug). 23 | $log = Log::get(); 24 | echo '$log: ', print_r($log, true), PHP_EOL; 25 | -------------------------------------------------------------------------------- /misc/BNI-http-response-body-maintenance.html: -------------------------------------------------------------------------------- 1 |
Nasabah Yth,
3 |Untuk meningkatkan kualitas layanan kepada nasabah, saat ini kami sedang melakukan maintenance sistem.
4 |Selama proses tersebut berlangsung, layanan transaksi melalui Internet Banking untuk sementara tidak dapat digunakan.
5 | 6 |Kami mohon maaf atas ketidaknyamanan ini.
7 | 8 |Informasi lebih lanjut, silakan menghubungi BNI Call di 1500046.
9 |-------------------------------------------------------------------------------------------------------------------------------------------------------------
10 | 11 |Dear Valued Customer,
12 | 13 |In order to maintain our quality services, we are currently performing system maintenance.
14 |During the aforementioned process, transaction services via Internet Banking will be temporarily unavailable.
15 | 16 |We apologize for any inconvenience.
17 | 18 |For further information, please call BNI Call at 1500046.
19 |" . __FILE__ . ":" . __LINE__ . "\r\n". 'var_dump(' . $debugname . '): '; var_dump($$debugname); echo "\r\n";
278 |
279 | // Cari tahu isian field image.x dan image.y
280 | // Kemungkinan ini adalah manipulasi javascript.
281 | // $image = $this->html->find();
282 |
283 |
284 | // unset($fields['txtUserId']);
285 | // $fields['value(user_id)'] = $this->username;
286 | // $fields['value(pswd)'] = $this->password;
287 | // $this->addStepFromReference('login_form');
288 | // $this->configuration('menu][bca_login_form][fields', $fields);
289 | // break;
290 | }
291 | }
292 |
293 | protected function mandiriPopulateImageFields()
294 | {
295 | // Untuk saat ini fix dulu ajah.
296 | return [
297 | 'image.x' => '0',
298 | 'image.y' => '0',
299 | ];
300 |
301 | }
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 | }
335 |
--------------------------------------------------------------------------------
/src/IBank/Modules/BCA/BCA.php:
--------------------------------------------------------------------------------
1 | parse(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'BCA-menu.yml'));
64 | $value += $yaml->parse(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'BCA-target.yml'));
65 | $value += $yaml->parse(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'BCA-reference.yml'));
66 | return $value;
67 | } catch (ParseException $e) {
68 | $this->log->error('Unable to parse the YAML string: {string}', ['string' => $e->getMessage()]);
69 | }
70 | }
71 |
72 | protected function init()
73 | {
74 | // Default nama file untuk keperluan debug.
75 | $_ = DIRECTORY_SEPARATOR;
76 | $this->configuration('temporary][browser][browser_history', '..' . $_ . 'debug' . $_ . 'history.log');
77 | $this->configuration('temporary][browser][browser_response_body', '..' . $_ . 'debug' . $_ . 'response_body.html');
78 | }
79 |
80 | /**
81 | * Override method.
82 | *
83 | * Set property information in object.
84 | *
85 | * @param $property string
86 | * Parameter dapat bernilai sebagai berikut:
87 | * - username
88 | * Username for login.
89 | * - password
90 | * Password for login.
91 | * - account
92 | * Account Number.
93 | * dan property lainnya yang dijelaskan pada parent::set().
94 | */
95 | public function set($property, $value)
96 | {
97 | switch ($property) {
98 | case 'username':
99 | case 'password':
100 | case 'account':
101 | case 'range':
102 | case 'sort':
103 | $this->{$property} = $value;
104 | break;
105 | }
106 | return parent::set($property, $value);
107 | }
108 |
109 | protected function executeBefore()
110 | {
111 | parent::executeBefore();
112 | switch ($this->target) {
113 | case 'logout':
114 | break;
115 |
116 | default:
117 | if (null === $this->username) {
118 | $this->log->error('Username belum didefinisikan.');
119 | throw new ExecuteException;
120 | }
121 | if (null === $this->password) {
122 | $this->log->error('Password belum didefinisikan.');
123 | throw new ExecuteException;
124 | }
125 | break;
126 | }
127 | switch ($this->target) {
128 | case 'get_transaction':
129 | if (null === $this->range) {
130 | // BCA tidak ada mini account statement seperti BNI, maka
131 | // jika null kita anggap saja today.
132 | $this->range = 'today';
133 | $this->log->notice('Range belum didefinisikan, otomatis mencari transaksi hari ini.');
134 | }
135 | $this->range = Range::create($this->range);
136 | // BCA paling lama adalah awal bulan dari 2 bulan lalu.
137 | $oldest = new \DateTime('first day of 2 months ago');
138 | if (!$this->range->comparison($oldest, 'less', 'start')) {
139 | // Tapi kalo masih di hari yang sama, ya gpp.
140 | if (!$this->range->isSameDay($oldest, 'start')) {
141 | $this->log->error('Tanggal Awal tidak boleh kurang dari hari pertama dari 2 bulan lalu: {date}', ['date' => $oldest->format('Y-m-d')]);
142 | throw new ExecuteException;
143 | }
144 | }
145 |
146 | // End date tidak boleh lewat dari hari ini.
147 | $now = new \DateTime();
148 | if (!$this->range->comparison($now, 'greater', 'end')) {
149 | // Tapi kalo masih di hari yang sama, ya gpp.
150 | if (!$this->range->isSameDay($now, 'end')) {
151 | $this->log->error('Tanggal Akhir tidak boleh melebihi Tanggal Hari Ini.');
152 | throw new ExecuteException;
153 | }
154 | }
155 | switch ($this->sort) {
156 | case 'asc':
157 | case 'desc':
158 | break;
159 |
160 | case 'ASC':
161 | case 'ascending':
162 | case 'ASCENDING':
163 | $this->sort = 'asc';
164 | break;
165 |
166 | case 'descending':
167 | case 'DESC':
168 | case 'DESCENDING':
169 | $this->sort = 'desc';
170 | break;
171 |
172 | default:
173 | $this->sort = 'desc';
174 | $this->log->notice('Transaksi otomatis diurut dengan pola descending.');
175 | break;
176 | }
177 |
178 | break;
179 |
180 | default:
181 | // Do something.
182 | break;
183 | }
184 |
185 |
186 | }
187 |
188 | /**
189 | * Override method.
190 | *
191 | * Set browser as mobile, and not use curl as library request.
192 | */
193 | protected function browserInit()
194 | {
195 | parent::browserInit();
196 | }
197 |
198 | protected function checkIndication($indication_name)
199 | {
200 | switch ($indication_name) {
201 | case 'home_page_anonymous':
202 | $form = $this->html->find('form[name=iBankForm][action=/authentication.do]');
203 | $this->configuration('temporary][form', $form);
204 | return ($form->length > 0);
205 |
206 | case 'home_page_authenticated':
207 | return ($this->html->find('frameset')->length > 0);
208 |
209 | case 'table_exists':
210 | $table = $this->html->find('table');
211 | $this->configuration('temporary][table', $table);
212 | return ($table->length > 0);
213 |
214 | case 'select_range_form':
215 | $form = $this->html->find('form[name=iBankForm][action=/accountstmt.do]');
216 | $this->configuration('temporary][form', $form);
217 | return ($form->length > 0);
218 |
219 | case 'redirect_to_main':
220 | $text = $this->html->text();
221 | return (strpos($text, "window.parent.location.href = 'main.jsp'") === 0);
222 |
223 | case 'redirect_to_main':
224 | $text = $this->html->text();
225 | return (strpos($text, "window.parent.location.href = 'main.jsp'") === 0);
226 |
227 | case 'table_transaction_page':
228 | $table = $this->html->find('body > table')->eq(2)->find('table')->eq(1);
229 | $this->configuration('temporary][table', $table);
230 | return ($table->length > 0);
231 | }
232 | }
233 |
234 | protected function visitBefore()
235 | {
236 | parent::visitBefore();
237 | }
238 |
239 | protected function visitAfter()
240 | {
241 | $this->configuration('bca_last_visit', date('c'));
242 | parent::visitAfter();
243 | }
244 |
245 | /**
246 | * Session expired setelah 8 menit, sesuai dengan informasi pada
247 | * javascript pada BCA.
248 | * Alternative handler for bca_check_session.
249 | */
250 | protected function bcaCheckSession()
251 | {
252 | $skip = false;
253 | // Menggunakan try catch, karena pembentukan object DateTime kalau gagal
254 | // akan throw Exception.
255 | try {
256 | $last_visit = $this->configuration('bca_last_visit');
257 | if ($last_visit === null) {
258 | throw new \Exception;
259 | }
260 | $now = new \DateTime;
261 | $expired = new \DateTime($last_visit);
262 | $expired->add(new \DateInterval('PT8M'));
263 | if ($now > $expired) {
264 | throw new \Exception;
265 | }
266 | }
267 | catch (\Exception $e) {
268 | $skip = true;
269 | }
270 | if ($skip) {
271 | $this->resetExecute();
272 | $this->addStepFromReference('home_page');
273 | throw new StepException;
274 | }
275 | }
276 |
277 | /**
278 | * Alternative handler for bca_set_referer.
279 | */
280 | protected function bcaSetReferer()
281 | {
282 | $menu_name = $this->step['menu'];
283 | $referer = $this->configuration("menu][bca_$menu_name][referer");
284 | if (empty($referer)) {
285 | return;
286 | }
287 | $language = $this->configuration('language');
288 | $part = null;
289 | switch ($language) {
290 | case 'en':
291 | $part = 'nav_bar_indo'; // Masih belum tahu.
292 | break;
293 |
294 | case 'id':
295 | $part = 'nav_bar_indo';
296 | break;
297 | }
298 |
299 | (null === $part) or $referer = Log::interpolate($referer, ['language' => $part]);
300 | $this->browser->headers('Referer', $referer);
301 | }
302 |
303 | /**
304 | * Alternative handler for bca_method_post.
305 | */
306 | protected function bcaMethodPost()
307 | {
308 | $this->browser->options('method', 'POST');
309 | }
310 |
311 | /**
312 | * Alternative handler for bca_parse_home_page_anonymous.
313 | */
314 | protected function bcaParseHomePageAnonymous()
315 | {
316 | switch ($this->target) {
317 | default:
318 | $form = $this->configuration('temporary][form');
319 | $fields = $form->preparePostForm('value(Submit)');
320 | unset($fields['txtUserId']);
321 | $fields['value(user_id)'] = $this->username;
322 | $fields['value(pswd)'] = $this->password;
323 | $this->addStepFromReference('login_form');
324 | $this->configuration('menu][bca_login_form][fields', $fields);
325 | break;
326 | }
327 | }
328 |
329 | /**
330 | * Alternative handler for bca_parse_home_page_authenticated.
331 | */
332 | protected function bcaParseHomePageAuthenticated()
333 | {
334 | switch ($this->target) {
335 | default:
336 | // Cari bahasa
337 | $src = $this->html->find('frameset > frame[name=menu]')->attr('src');
338 | if (strpos($src, 'nav_bar_indo') === 0) {
339 | $this->configuration('language', 'id');
340 | }
341 | else {
342 | $this->configuration('language', 'en');
343 | }
344 | break;
345 | }
346 | }
347 |
348 | /**
349 | * Alternative handler for bca_parse_balance_inquiry_page.
350 | */
351 | protected function bcaParseBalanceInquiryPage()
352 | {
353 | switch ($this->target) {
354 | default:
355 | $table = $this->configuration('temporary][table');
356 | $info = $table->eq('2')->extractTable(true);
357 | $balance = isset($info[1][3]) ? $info[1][3] : null;
358 | $this->result = $balance;
359 | break;
360 | }
361 | }
362 |
363 | /**
364 | * Alternative handler for bca_parse_select_range_form.
365 | */
366 | protected function bcaParseSelectRangeForm()
367 | {
368 | // BCA memiliki keunikan dalam pencarian mutasi rekening.
369 | // Untuk harian, hanya bisa 31 hari terakhir dari hari ini.
370 | // Setelah itu adalah keseluruhan dari awal sampai akhir hari 2 bulan
371 | // lalu, dan 1 bulan lalu.
372 | // Ribet amat, yak.
373 | // 31 hari terakhir itu berarti total hari dari sekarang adalah 31 hari.
374 | // artinya ada jeda 30 hari antara hari ini dengan hari terakhir.
375 | // Bila hari ini adalah 2 Februari 2016, maka jika hari terakhir itu
376 | // 1 Januari 2016, maka tidak valid. tapi jika 2 januari 2016, maka
377 | // valid.
378 |
379 | $form = $this->configuration('temporary][form');
380 | $fields = $form->preparePostForm('value(submit1)');
381 |
382 | // Cari tahu rekening.
383 | $options_rekening = $this->html->find('select[name=value(D1)] > option')->getElements();
384 | $fields['value(D1)'] = $this->bcaSelectAccountGetValueFromOptionsElement($options_rekening);
385 |
386 | $type = null; // daily, monthly.
387 |
388 | // Check apakah ini revisit.
389 | $is_revisit = $this->configuration('temporary][revisit_account_statement_page');
390 | if ($is_revisit) {
391 | // Copot bulan selanjutnya.
392 | $_month = key($this->range);
393 | $month = array_shift($this->range);
394 | reset($this->range);
395 | // Jika month adalah bulan ini, maka gunakan pencarian tipe harian.
396 | // Jika month adalah bulan kemarin, maka gunakan pencarian tipe bulanan.
397 | $now_month = date('Y-m');
398 | $last_month = Range::getPrevMonth($now_month);
399 | if ($_month == $now_month) {
400 | $type = 'daily';
401 | }
402 | elseif ($_month == $last_month) {
403 | $type = 'monthly';
404 | }
405 | }
406 | else {
407 | // Bukan revisit, maka:
408 | $limit = new \DateTime('31 days ago');
409 | if ($this->range->isSameDay($limit, 'start') || $this->range->comparison($limit, '<', 'start')) {
410 | $type = 'daily';
411 | $month = $this->range;
412 | }
413 | else {
414 | $type = 'monthly';
415 | // Set over range.
416 | $this->configuration('temporary][over_range', true);
417 | // Set need to filter.
418 | $this->configuration('temporary][filter_transaction', true);
419 | // Simpan original range, karena akan dipecah.
420 | $this->configuration('temporary][range', $this->range);
421 | // Atur ulang range.
422 | $this->range = $this->range->splitPerMonth();
423 | $_month = key($this->range);
424 | reset($this->range);
425 | // Copot bulan pertama.
426 | $month = array_shift($this->range);
427 | // $month bisa merupakan start_date di tanggal 13 dan end date
428 | // di tanggal 31. tapi karena bca selalu di tanggal awal, maka:
429 | // Rebuild ulang, agar tanggal awal menjadi 1 dan tanggal akhir
430 | // menjadi last (28/29/30/31.
431 | $month = Range::create("first day of $_month ~ last day of $_month");
432 | }
433 | }
434 |
435 | // Simpan informasi $month, diperlukan untuk
436 | // method ::bcaConvertDateTransaction()
437 | $this->configuration('temporary][month', $month);
438 |
439 | // Positioning.
440 | switch ($type) {
441 | case 'daily':
442 | // Pilih field "Mutasi Harian".
443 | $fields['value(r1)'] = '1';
444 | unset($fields['value(x)']);
445 | $fields['value(startDt)'] = $month->format(self::BCA_DATE_FORMAT_DAILY_DATE, 'start');
446 | $fields['value(startMt)'] = $month->format(self::BCA_DATE_FORMAT_DAILY_MONTH, 'start');
447 | $fields['value(startYr)'] = $month->format(self::BCA_DATE_FORMAT_DAILY_YEAR, 'start');
448 | $fields['value(endDt)'] = $month->format(self::BCA_DATE_FORMAT_DAILY_DATE, 'end');
449 | $fields['value(endMt)'] = $month->format(self::BCA_DATE_FORMAT_DAILY_MONTH, 'end');
450 | $fields['value(endYr)'] = $month->format(self::BCA_DATE_FORMAT_DAILY_YEAR, 'end');
451 | $fields['value(fDt)'] = '';
452 | $fields['value(tDt)'] = '';
453 | break;
454 |
455 | case 'monthly':
456 | // Isi field r1
457 | // Pilih field "Mutasi Bulanan".
458 | $fields['value(r1)'] = '2';
459 | // Isi field x
460 | // Harusnya sudah ada variable $_month (string) dan
461 | // $month (object Range).
462 | $now_month = date('Y-m');
463 | $last_month = Range::getPrevMonth($now_month);
464 | $two_last_month = Range::getPrevMonth($last_month);
465 | if ($last_month == $_month) {
466 | $fields['value(x)'] = '1';
467 | }
468 | elseif ($two_last_month == $_month) {
469 | $fields['value(x)'] = '2';
470 | }
471 | // Isi field fDt & tDt.
472 | $fields['value(fDt)'] = $month->format(self::BCA_DATE_FORMAT, 'start');
473 | $fields['value(tDt)'] = $month->format(self::BCA_DATE_FORMAT, 'end');
474 |
475 | // Anomali BCA.
476 | // Kasus seperti ini: milih request bulan desember 2015
477 | // lalu oleh javascriptnya BCA dimodifikasi menjadi tanggal masa
478 | // depan yakni menjadi 01122016 ~ 31122016. dan saat disubmit
479 | // ternyata post fieldnya bener-bener tanggal masa depan yakni
480 | // 01122016 ~ 31122016. Tapi respon yang muncul tetap ke desember
481 | // 2015 (01122015 ~ 31122015). Hadeh, ke-tidakkonsisten-an ini
482 | // mengganggu coding.
483 | // Hack dimulai:
484 | // Modifikasi, tahun apapun menjadi tahun saat ini.
485 | $fields['value(fDt)'] = substr($fields['value(fDt)'], 0, -4) . date('Y');
486 | $fields['value(tDt)'] = substr($fields['value(tDt)'], 0, -4) . date('Y');
487 |
488 | unset($fields['value(startDt)']);
489 | unset($fields['value(endDt)']);
490 | unset($fields['value(startMt)']);
491 | unset($fields['value(endMt)']);
492 | unset($fields['value(startYr)']);
493 | unset($fields['value(endYr)']);
494 | break;
495 |
496 | default:
497 | // Do something.
498 | break;
499 | }
500 | // Set ke menu.
501 | $this->configuration("menu][bca_account_statement_page_view][fields", $fields);
502 |
503 |
504 | }
505 |
506 | protected function bcaParseRedirectToMain()
507 | {
508 | switch ($this->target) {
509 | default:
510 | $this->configuration('bca_last_visit', null);
511 | $this->resetExecute();
512 | $this->addStepFromReference('home_page');
513 | break;
514 | }
515 | }
516 |
517 | /**
518 | * Jika tidak ditemukan, maka akan throw ke VisitException.
519 | */
520 | protected function bcaSelectAccountGetValueFromOptionsElement(Array $element_options)
521 | {
522 | $found = false;
523 | while ($each = array_shift($element_options)) {
524 | $extract = ParseHTMLAdvanced::extract($each);
525 | $text = ParseHTMLAdvanced::extractValueOnly($each);
526 | if ($text == $this->account) {
527 | if (isset($extract['a']['value'])) {
528 | $found = $extract['a']['value'];
529 | break;
530 | }
531 | }
532 | }
533 | if (false === $found) {
534 | $this->log->error('Nomor Rekening tidak ditemukan.');
535 | throw new VisitException;
536 | }
537 | return $found;
538 | }
539 |
540 |
541 | protected function bcaFilterTransactionTable($tables)
542 | {
543 | $ref = IBank::reference('table_header_account_statement');
544 | $ref = array_flip($ref);
545 |
546 | $transactions = [];
547 | while (!empty($tables)) {
548 | $transaction = [];
549 | $table = array_shift($tables);
550 | if (count($table) == 6) {
551 | list($tgl, $keterangan, $cab, $mutasi_1, $mutasi_2, $saldo) = $table;
552 | $transaction['date'] = $this->bcaConvertDateTransaction($tgl);
553 | $keterangan = implode('', $keterangan);
554 | $keterangan = preg_replace('/<[^>]+>/', ' ', $keterangan);
555 | $keterangan = preg_replace('/\s\s+/', ' ', $keterangan);
556 | $keterangan = trim($keterangan);
557 | $transaction['description'] = $keterangan;
558 | $transaction['bca_branch'] = $cab;
559 | $transaction['bca_date'] = $tgl;
560 | $transaction['amount'] = $mutasi_1;
561 | $transaction['type'] = $mutasi_2;
562 | $transaction['balance'] = $saldo;
563 | $transaction['no'] = null;
564 | $transaction['id'] = null;
565 | }
566 | $transactions[] = array_merge($ref, $transaction);
567 | }
568 | return $transactions;
569 | }
570 |
571 | protected function bcaParseTransactionPage()
572 | {
573 | $table = $this->configuration('temporary][table');
574 | $info = $table->extractTable(true);
575 |
576 | // Buang baris awal.
577 | array_shift($info);
578 | $transaction = $this->bcaFilterTransactionTable($info);
579 |
580 | $temporary_result = $this->configuration('temporary][result');
581 | if (null === $temporary_result) {
582 | $temporary_result = [];
583 | }
584 | $temporary_result = array_merge($temporary_result, $transaction);
585 | $this->configuration('temporary][result', $temporary_result);
586 | }
587 |
588 | /**
589 | * bca_error_login
590 | */
591 | protected function bcaErrorLogin()
592 | {
593 | $elements = $this->html->find('script')->getElements();
594 | $error = 'Error login from server.';
595 | foreach ($elements as $element) {
596 | if (strpos($element, 'alert(err);') !== false) {
597 | if (preg_match('/var err=\'(.*)\';/', $element, $m)) {
598 | $error = $m[1];
599 | }
600 | }
601 | }
602 | $this->log->error($error);
603 | throw new ExecuteException;
604 | }
605 |
606 | /**
607 | * bca_check_over_range
608 | */
609 | protected function bcaCheckOverRange()
610 | {
611 | $finish = false;
612 | $is_over_range = $this->configuration('temporary][over_range');
613 | if ($is_over_range) {
614 | if (empty($this->range)) {
615 | // Hapus informasi over_range.
616 | $this->configuration('temporary][over_range', false);
617 | // Tambah step finishing.
618 | $finish = true;
619 | }
620 | else {
621 | $this->addStepFromReference('revisit_account_statement_page');
622 | $this->configuration('temporary][revisit_account_statement_page', true);
623 | }
624 | }
625 | else {
626 | $finish = true;
627 | }
628 |
629 | if ($finish) {
630 | $this->addStepFromReference('transaction_finishing');
631 | }
632 | }
633 |
634 | /**
635 | * bca_transaction_finishing
636 | */
637 | protected function bcaTransactionFinishing()
638 | {
639 | $temporary_result = $this->configuration('temporary][result');
640 | $this->result = $temporary_result;
641 |
642 | if ($this->configuration('temporary][filter_transaction')) {
643 | $this->addStepFromReference('filter_transaction');
644 | }
645 | // Hasil BCA selalu ascending.
646 | if ($this->sort == 'desc') {
647 | krsort($this->result);
648 | $this->result = array_values($this->result);
649 | }
650 | }
651 |
652 | protected function bcaClearLastVisit()
653 | {
654 | $this->configuration('bca_last_visit', null);
655 | $this->result = 'Logout Success';
656 | }
657 |
658 | protected function bcaFilterTransaction()
659 | {
660 | $result = [];
661 | // Get original range.
662 | $range = $this->configuration('temporary][range');
663 | foreach ($this->result as $each) {
664 | $date = new \DateTime($each['date']);
665 | if ($range->isBetween($date)) {
666 | $result[] = $each;
667 | }
668 | unset($date);
669 | }
670 | $this->result = $result;
671 | }
672 |
673 | /**
674 | *
675 | * Beberapa contoh string yang ada di BCA adalah
676 | * - 12/01
677 | * artinya 12 januari tahun ini.
678 | * - PEND
679 | * artinya **mungkin** tanggal hari ini, mungkin kemarin, karena ini
680 | * muncul pada transaksi yang baru saja terjadi.
681 | */
682 | protected function bcaConvertDateTransaction($string)
683 | {
684 | $parse = $this->_bcaConvertDateTransaction($string);
685 | if (false === $parse) {
686 | $this->log->error('Gagal mengenali date: {name}', ['name' => $string]);
687 | throw new VisitException;
688 | }
689 | if ('' === $parse) {
690 | return '';
691 | }
692 | list($d, $m) = $parse;
693 | $is_over_range = $this->configuration('temporary][over_range');
694 | if ($is_over_range) {
695 | $month = $this->configuration('temporary][month');
696 | $Y = $month->format('Y', 'end');
697 | }
698 | else {
699 | // Jika tidak over range, maka tipe transaction
700 | // hanya daily.
701 | $Y = $this->range->format('Y', 'end');
702 | $end_month = $this->range->format('m', 'end');
703 | if ($m == '12' && $end_month == '01') {
704 | $Y -= 1;
705 | }
706 | }
707 | return "$Y-$m-$d";
708 | }
709 |
710 | protected function _bcaConvertDateTransaction($string)
711 | {
712 | $result = false;
713 | $string = trim($string);
714 | if ($string === 'PEND') {
715 | $result = '';
716 | }
717 | elseif (preg_match('/^(\d{1,2})\/(\d{1,2})$/', $string, $m)) {
718 | // Perlu valid date.
719 | $result = array($m[1], $m[2]);
720 | }
721 | return $result;
722 | }
723 | }
724 |
--------------------------------------------------------------------------------
/src/IBank/Modules/BNI/BNI.php:
--------------------------------------------------------------------------------
1 | parse(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'BNI-menu.yml'));
62 | $value += $yaml->parse(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'BNI-target.yml'));
63 | $value += $yaml->parse(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'BNI-reference.yml'));
64 | return $value;
65 | } catch (ParseException $e) {
66 | $this->log->error('Unable to parse the YAML string: {string}', ['string' => $e->getMessage()]);
67 | }
68 | }
69 |
70 | protected function init()
71 | {
72 | // Default nama file untuk keperluan debug.
73 | $_ = DIRECTORY_SEPARATOR;
74 | $this->configuration('temporary][browser][browser_history', '..' . $_ . 'debug' . $_ . 'history.log');
75 | $this->configuration('temporary][browser][browser_response_body', '..' . $_ . 'debug' . $_ . 'response_body.html');
76 | }
77 |
78 | /**
79 | * Override method.
80 | *
81 | * Set property information in object.
82 | *
83 | * @param $property string
84 | * Parameter dapat bernilai sebagai berikut:
85 | * - username
86 | * Username for login.
87 | * - password
88 | * Password for login.
89 | * - account
90 | * Account Number.
91 | * dan property lainnya yang dijelaskan pada parent::set().
92 | */
93 | public function set($property, $value)
94 | {
95 | switch ($property) {
96 | case 'username':
97 | case 'password':
98 | case 'account':
99 | case 'range':
100 | case 'sort':
101 | $this->{$property} = $value;
102 | break;
103 | }
104 | return parent::set($property, $value);
105 | }
106 |
107 | /**
108 | * Override parent::executeBefore()
109 | * Verifikasi kebutuhan sebelum melanjutkan execute.
110 | *
111 | * Catatan tentang validitas mutasi rekening di BNI.
112 | *
113 | * - Tanggal transaksi paling lama yang bisa diambil adalah 6 bulan dari
114 | * hari ini. Jika sekarang tanggal 25 januari 2016, maka bila start
115 | * date 25-Jul-2015, error yang muncul adalah: "Tanggal Awal tidak boleh
116 | * Melebihi 6 Bulan dari Tanggal Hari Ini" dan akan valid jika start_date
117 | * 24-Jul-2016.
118 | *
119 | * - End date yang lewat dari hari ini, maka error yang muncul adalah:
120 | * "Tanggal Akhir tidak boleh melebihi tanggal hari ini".
121 | *
122 | * - Tiap sekali request, maka interval tidak boleh lebih 31 hari, jika
123 | * lebih dari 31 hari, error yang muncul adalah: "Transaksi anda tidak
124 | * dapat diproses. Periode tanggal yang anda pilih lebih dari 31 hari.
125 | * Silahkan masukkan periode tanggal sesuai ketentuan.".
126 | *
127 | * - Untuk support interval lebih dari 31 hari, maka module BNI melakukan
128 | * split interval, kemudian melakukan request secara looping.
129 | *
130 | * - Tanggal yang tidak valid (contoh 32-Jul-2015) atau format yang tidak
131 | * valid (contoh 1-Aug-2015) maka error yang muncul adalah "Tanggal Akhir
132 | * harus menggunakan format yang telah ditentukan dan tanggal yang
133 | * valid".
134 | */
135 | protected function executeBefore()
136 | {
137 | parent::executeBefore();
138 | if (null === $this->username) {
139 | $this->log->error('Username belum didefinisikan.');
140 | throw new ExecuteException;
141 | }
142 | if (null === $this->password) {
143 | $this->log->error('Password belum didefinisikan.');
144 | throw new ExecuteException;
145 | }
146 | switch ($this->target) {
147 | case 'get_range_transaction':
148 | if (null === $this->range) {
149 | $this->log->error('Range belum didefinisikan.');
150 | throw new ExecuteException;
151 | }
152 | switch ($this->range) {
153 | case 'now':
154 | case 'today':
155 | case 'last week':
156 | case 'last month':
157 | break;
158 |
159 | default:
160 | // Verifikasi rangenya.
161 | $this->range = Range::create($this->range);
162 | if (!$this->range->is_start_valid) {
163 | $this->log->notice('Tanggal awal tidak valid. Tanggal otomatis diganti menjadi waktu saat ini.');
164 | }
165 | if (!$this->range->is_end_valid) {
166 | $this->log->notice('Tanggal awal tidak valid. Tanggal otomatis diganti menjadi waktu saat ini.');
167 | }
168 | // Start date tidak boleh lebih dari 6 bulan sejak hari
169 | // ini.
170 | $oldest = new \DateTime('6 month ago');
171 | // Masih kudu dikurangi satu hari lagi agar tidak
172 | // error (lihat catatan pada doc comment fungsi ini).
173 | $oldest->sub(new \DateInterval('P1D'));
174 | if (!$this->range->isSameDay($oldest, 'start') && !$this->range->comparison($oldest, 'less', 'start')) {
175 | $this->log->error('Tanggal Awal tidak boleh kurang dari 6 bulan lalu: {date}', ['date' => $oldest->format('Y-m-d')]);
176 | throw new ExecuteException;
177 | }
178 |
179 | // End date tidak boleh lewat dari hari ini.
180 | $now = new \DateTime();
181 | if (!$this->range->isSameDay($now, 'end') && !$this->range->comparison($now, 'greater', 'end')) {
182 | $this->log->error('Tanggal Akhir tidak boleh melebihi Tanggal Hari Ini.');
183 | throw new ExecuteException;
184 | }
185 | }
186 |
187 | case 'get_last_transaction':
188 | switch ($this->sort) {
189 | case 'asc':
190 | case 'desc':
191 | break;
192 |
193 | case 'ASC':
194 | case 'ascending':
195 | case 'ASCENDING':
196 | $this->sort = 'asc';
197 | break;
198 |
199 | case 'descending':
200 | case 'DESC':
201 | case 'DESCENDING':
202 | $this->sort = 'desc';
203 | break;
204 |
205 | default:
206 | $this->sort = 'desc';
207 | $this->log->notice('Transaksi otomatis diurut dengan pola descending.');
208 | break;
209 | }
210 | break;
211 |
212 | default:
213 | // Do something.
214 | break;
215 | }
216 |
217 |
218 | switch ($this->target) {
219 |
220 | }
221 | }
222 |
223 | protected function executeAfter()
224 | {
225 | parent::executeAfter();
226 | // Memastikan bahwa url home sudah ada pada configuration.
227 | $url = $this->configuration('menu][bni_home_page][url');
228 | if (null === $url && null !== $this->html) {
229 | $form = $this->html->find('form');
230 | $url = $form->attr('action');
231 | $fields = $form->preparePostForm('__HOME__');
232 | $this->configuration('menu][bni_home_page][url', $url);
233 | $this->configuration('menu][bni_home_page][fields', $fields);
234 | }
235 | }
236 |
237 | /**
238 | * Override method.
239 | *
240 | * Set browser as mobile, and not use curl as library request.
241 | */
242 | protected function browserInit()
243 | {
244 | parent::browserInit();
245 | $user_agent_mobile = $this->configuration('user_agent_mobile');
246 | if (!$user_agent_mobile) {
247 | $user_agent = $this->browser->getUserAgent('Mobile Browser');
248 | $this->configuration('user_agent', $user_agent);
249 | $this->configuration('user_agent_mobile', true);
250 | $this->browser->options('user_agent', $user_agent);
251 | }
252 | $this->browser->curl(false);
253 | }
254 |
255 | /**
256 | *
257 | */
258 | protected function visitAfter()
259 | {
260 | $this->configuration('bni_last_visit', date('c'));
261 | // Untuk semua visit.
262 | // Hapus url, agar tidak tersimpan di configuration.
263 | // Karena url bersifat dinamis.
264 | $menu_name = $this->step['menu'];
265 | $this->configuration("menu][$menu_name][url", null);
266 | // Baru jalankan parent.
267 | parent::visitAfter();
268 | }
269 |
270 | /**
271 | * Karena visitAfter menghapus url, maka kembalikan default.
272 | */
273 | protected function resetExecuteAfter()
274 | {
275 | $this->configuration('menu][bni_home_page][url', self::BNI_MAIN_URL);
276 | }
277 |
278 | /**
279 | * Memastikan bahwa halaman mengandung indikasi yang dibutuhkan untuk
280 | * nantinya bisa diparsing sesuai dengan target.
281 | */
282 | protected function checkIndication($indication_name)
283 | {
284 | switch ($indication_name) {
285 | case '404_page':
286 | $text = $this->html->find('span#Step1')->text();
287 | $position = strpos($text, '404');
288 | return is_int($position);
289 |
290 | case 'home_page_authenticated':
291 | return ($this->html->find('span#CurrentProfileDisp')->length > 0);
292 |
293 | case 'home_page_anonymous':
294 | return ($this->html->find('table#Language_table')->length > 0);
295 |
296 | case 'form_exists':
297 | return ($this->html->find('form')->length > 0);
298 |
299 | case 'login_error':
300 | case 'select_range_error':
301 | return ($this->html->find('#Display_MConError')->length > 0);
302 |
303 | case 'table_balance':
304 | return ($this->html->find('table[id~=BalanceDisplayTable]')->eq(1)->length > 0);
305 |
306 | case 'table_account':
307 | return ($this->html->find('table#AccountMenuList_table')->length > 0);
308 |
309 | case 'mini_statement_select_account_number':
310 | return ($this->html->find('select[name=MiniStmt]')->length > 0);
311 |
312 | case 'mini_statement_page':
313 | return ($this->html->find('input[name=PageName][value=OperMiniAccIDSelectRq]')->length > 0);
314 |
315 | case 'select_range_page':
316 | return ($this->html->find('#Search_Criteria_tr')->length > 0);
317 |
318 | case 'table_transaction_page':
319 | return ($this->html->find('input[name=page][value=FullStmtInqRq]')->length > 0);
320 |
321 | case 'session_destroy':
322 | return ($this->html->find('input[name=page][value=SessionErrorMessage]')->length > 0);
323 | }
324 | }
325 |
326 | /**
327 | * Parsing menu "home_page" context "authenticated" sesuai dengan kebutuhan
328 | * pada property $target.
329 | * Alternative handler for bni_parse_home_page_authenticated.
330 | */
331 | protected function bniParseHomePageAuthenticated()
332 | {
333 | switch ($this->target) {
334 | case 'get_balance':
335 | case 'get_last_transaction':
336 | case 'get_range_transaction':
337 | $url_account_page = $this->html->find('td a')->eq(0)->attr('href');
338 | if (empty($url_account_page)) {
339 | $this->log->error('Url for menu "account_page" not found.');
340 | throw new VisitException;
341 | }
342 | $this->configuration('menu][bni_account_page][url', $url_account_page);
343 | break;
344 | }
345 | }
346 |
347 | /**
348 | * Parsing menu "home_page" context "anonymous" sesuai dengan kebutuhan
349 | * pada property $target.
350 | * Alternative handler for bni_parse_home_page_anonymous.
351 | */
352 | protected function bniParseHomePageAnonymous()
353 | {
354 | switch ($this->target) {
355 | // Apapun targetnya, aktivitasnya sama.
356 | default:
357 | // Belum login, maka tambah langkah baru.
358 | $this->addStepFromReference('home_page');
359 |
360 | // Cari tahu bahasa situs, penting untuk parsing yang
361 | // terkait bahasa.
362 | if ($this->html->find('span.Languageleftselect')->length) {
363 | $this->configuration('language', 'id');
364 | }
365 | elseif ($this->html->find('span.Languagerightselect')->length) {
366 | $this->configuration('language', 'en');
367 | }
368 |
369 | // Cari url untuk ke halaman login_page.
370 | $url_login_page = $this->html->find('#RetailUser')->attr('href');
371 | if (empty($url_login_page)) {
372 | $this->log->error('Url for menu login_page not found.');
373 | throw new VisitException;
374 | }
375 | $this->configuration('menu][bni_login_page][url', $url_login_page);
376 | break;
377 | }
378 | }
379 |
380 | /**
381 | * Parsing menu "login_page" sesuai dengan kebutuhan
382 | * pada property $target.
383 | * Alternative handler for bni_parse_login_page.
384 | */
385 | protected function bniParseLoginPage()
386 | {
387 | switch ($this->target) {
388 | // Apapun targetnya, aktivitasnya sama.
389 | default:
390 | $url = $this->html->find('form')->attr('action');
391 | if (empty($url)) {
392 | $this->log->error('Url for form "login_form" not found.');
393 | throw new VisitException;
394 | }
395 | if (empty($this->username) || empty($this->password)) {
396 | $this->log->error('Username and Password required.');
397 | throw new VisitException;
398 | }
399 | $fields = $this->html->find('form')->extractForm();
400 | $fields['__AUTHENTICATE__'] = 'Login';
401 | $fields['CorpId'] = $this->username;
402 | $fields['PassWord'] = $this->password;
403 | $this->configuration('menu][bni_login_form][url', $url);
404 | $this->configuration('menu][bni_login_form][fields', $fields);
405 | break;
406 | }
407 | }
408 |
409 | /**
410 | * Parsing menu "account_page" sesuai dengan kebutuhan
411 | * pada property $target.
412 | * Alternative handler for bni_parse_account_page.
413 | */
414 | protected function bniParseAccountPage()
415 | {
416 | switch ($this->target) {
417 | case 'get_balance':
418 | $url_balance_inquiry_page = $this->html->find('td a')->eq(0)->attr('href');
419 | if (empty($url_balance_inquiry_page)) {
420 | $this->log->error('Url for menu "balance_inquiry_page" not found.');
421 | throw new VisitException;
422 | }
423 | $this->configuration('menu][bni_balance_inquiry_page][url', $url_balance_inquiry_page);
424 | break;
425 |
426 | case 'get_last_transaction':
427 | $url_mini_statement_page = $this->html->find('td a')->eq(1)->attr('href');
428 | if (empty($url_mini_statement_page)) {
429 | $this->log->error('Url for menu "mini_statement_page" not found.');
430 | throw new VisitException;
431 | }
432 | $this->configuration('menu][bni_mini_statement_page][url', $url_mini_statement_page);
433 | break;
434 |
435 | case 'get_range_transaction':
436 | $url_transaction_history_page = $this->html->find('td a')->eq(2)->attr('href');
437 | if (empty($url_transaction_history_page)) {
438 | $this->log->error('Url for menu "transaction_history_page" not found.');
439 | throw new VisitException;
440 | }
441 | $this->configuration('menu][bni_transaction_history_page][url', $url_transaction_history_page);
442 | break;
443 | }
444 | }
445 |
446 | /**
447 | * Parsing menu "account_type_form" sesuai dengan kebutuhan
448 | * pada property $target.
449 | * Alternative handler for bni_parse_account_type_form.
450 | */
451 | protected function bniParseAccountTypeForm()
452 | {
453 | switch ($this->target) {
454 | case 'get_balance':
455 | case 'get_last_transaction':
456 | case 'get_range_transaction':
457 | $form = $this->html->find('form');
458 | $url = $form->attr('action');
459 | if (empty($url)) {
460 | $this->log->error('Url for form "account_type_form" not found.');
461 | throw new VisitException;
462 | }
463 | $fields = $form->preparePostForm('AccountIDSelectRq');
464 | //
465 | $fields['MAIN_ACCOUNT_TYPE'] = $this->account_type;
466 | $this->configuration('menu][bni_account_type_form][url', $url);
467 | $this->configuration('menu][bni_account_type_form][fields', $fields);
468 | break;
469 | }
470 | }
471 |
472 | /**
473 | * Parsing menu "account_number_form" sesuai dengan kebutuhan
474 | * pada property $target.
475 | * Alternative handler for bni_parse_account_number_form.
476 | */
477 | protected function bniParseAccountNumberForm()
478 | {
479 | switch ($this->target) {
480 | case 'get_balance':
481 | $form = $this->html->find('form');
482 | $url = $form->attr('action');
483 | if (empty($url)) {
484 | $this->log->error('Url for form "account_number_form" not found.');
485 | throw new VisitException;
486 | }
487 | $fields = $form->preparePostForm('BalInqRq');
488 | // Todo, support multi account number.
489 | // $fields['acc1'] = '';
490 | $this->configuration('menu][bni_account_number_form][url', $url);
491 | $this->configuration('menu][bni_account_number_form][fields', $fields);
492 | break;
493 |
494 | case 'get_last_transaction':
495 | $form = $this->html->find('form');
496 | $url = $form->attr('action');
497 | if (empty($url)) {
498 | $this->log->error('Url for form "account_number_form" not found.');
499 | throw new VisitException;
500 | }
501 | $fields = $form->preparePostForm('Go');
502 | // Cari nomor rekening.
503 | $value = null;
504 | foreach ($fields['MiniStmt'] as $number) {
505 | if (strpos($number, $this->account) !== false) {
506 | $value = $number;
507 | }
508 | }
509 | $fields['MiniStmt'] = $value;
510 | $this->configuration('menu][bni_account_number_form][url', $url);
511 | $this->configuration('menu][bni_account_number_form][fields', $fields);
512 | break;
513 | }
514 | }
515 |
516 | /**
517 | * Parsing menu "balance_inquiry_page" sesuai dengan kebutuhan
518 | * pada property $target.
519 | * Alternative handler for bni_parse_balance_inquiry_page.
520 | */
521 | protected function bniParseBalanceInquiryPage()
522 | {
523 | switch ($this->target) {
524 | case 'get_balance':
525 | // Get Balance.
526 | $indication_table_balance = $this->html->find('table[id~=BalanceDisplayTable]')->eq(1);
527 | $span = $indication_table_balance->find('tr#Row5_5 td#Row5_5_column2 span');
528 | $balance = $span->text();
529 | $this->result = $balance;
530 |
531 | // Keep information of home_page
532 | $this->bniSaveUrlHomePage();
533 | break;
534 | }
535 | }
536 |
537 | // Keep information of home_page
538 | protected function bniSaveUrlHomePage()
539 | {
540 | $form = $this->html->find('form');
541 | $url = $form->attr('action');
542 | $fields = $form->preparePostForm('__HOME__');
543 | $this->configuration('menu][bni_home_page][url', $url);
544 | $this->configuration('menu][bni_home_page][fields', $fields);
545 | }
546 |
547 | /**
548 | * Parsing menu "404_page" sesuai dengan kebutuhan
549 | * pada property $target.
550 | * Alternative handler for bni_parse_404_page.
551 | */
552 | protected function bniParse404Page()
553 | {
554 | switch ($this->target) {
555 | default:
556 | // 404 terjadi, maka tambah langkah baru.
557 | // Temukan url login.
558 | $url = $this->html->find('a#Login')->attr('href');
559 | if (empty($url)) {
560 | $this->log->error('Url for menu "login_page" not found.');
561 | throw new VisitException;
562 | }
563 | $this->configuration('menu][bni_login_page][url', $url);
564 | $this->addStepFromReference('404_page');
565 | break;
566 | }
567 | }
568 |
569 | /**
570 | * Alternative handler for bni_parse_login_form_error.
571 | */
572 | protected function bniParseLoginFormError()
573 | {
574 | $text = $this->html->find('#Display_MConError')->text();
575 | $text = preg_replace('/\s\s+/', ' ', $text);
576 | $text = trim($text);
577 | $this->log->error('Login failed. Message: {text}', ['text' => $text]);
578 | throw new VisitException;
579 | }
580 |
581 | /**
582 | * Alternative handler for bni_parse_select_range_error.
583 | */
584 | protected function bniParseSelectRangeError()
585 | {
586 | $text = $this->html->find('#Display_MConError')->text();
587 | $text = preg_replace('/\s\s+/', ' ', $text);
588 | $text = trim($text);
589 | $this->log->error('Select Range failed. Message: {text}', ['text' => $text]);
590 | throw new VisitException;
591 | }
592 |
593 | /**
594 | * Alternative handler for bni_parse_mini_statement_page.
595 | */
596 | protected function bniParseMiniStatementPage()
597 | {
598 | switch ($this->target) {
599 | case 'get_last_transaction':
600 | $language = $this->configuration('language');
601 | $tables = $this->html->find('div#TitleBar > table')->extractTable(true);
602 | if (empty($tables)) {
603 | $this->log->error('Table for Statement not found.');
604 | throw new VisitException;
605 | }
606 | $transactions = [];
607 | while ($table = array_shift($tables)) {
608 | if (isset($table[0]) && isset($table[1])) {
609 | $info = $language == 'id' ? $this->bniTranslate($table[0]) : $table[0];
610 | $value = $table[1];
611 | switch ($info) {
612 | case 'Transaction Date':
613 | $transaction = [];
614 | $transaction['date'] = $value;
615 | break;
616 |
617 | case 'Transaction Remarks':
618 | $transaction['detail'] = $value;
619 | break;
620 |
621 | case 'Amount type':
622 | $transaction['type'] = $value;
623 | break;
624 |
625 | case 'Amount':
626 | $transaction['amount'] = $value;
627 | break;
628 |
629 | case 'Account Balance':
630 | $transaction['balance'] = $value;
631 | $transactions[] = $transaction;
632 | break;
633 | }
634 | }
635 | }
636 | // Sort.
637 | if ($this->sort == 'asc') {
638 | krsort($transactions);
639 | $transactions = array_values($transactions);
640 | }
641 | // Set to result.
642 | $this->result = $transactions;
643 |
644 | // Keep information of home_page.
645 | $this->bniSaveUrlHomePage();
646 | // Tapi ada yang perlu diedit, karena isian pilih rekening
647 | // ternyata element select sehingga perlu kita ganti.
648 | $fields = $this->configuration('menu][bni_home_page][fields');
649 | $value = null;
650 | foreach ($fields['MiniStmt'] as $number) {
651 | if (strpos($number, $this->account) !== false) {
652 | $value = $number;
653 | }
654 | }
655 | $fields['MiniStmt'] = $value;
656 | $this->configuration('menu][bni_home_page][fields', $fields);
657 | break;
658 | }
659 | }
660 |
661 | /**
662 | * Parsing menu "select_range_page" sesuai dengan kebutuhan
663 | * pada property $target.
664 | * Alternative handler for bni_parse_select_range_page.
665 | */
666 | protected function bniParseSelectRangePage()
667 | {
668 | switch ($this->target) {
669 | case 'get_range_transaction':
670 | $form = $this->html->find('form');
671 | $url = $form->attr('action');
672 | $fields = $form->preparePostForm('FullStmtInqRq');
673 | // Untuk kasus range yang spesifik dan sering digunakan, maka
674 | // tidak perlu diconvert ke object Range. Langsung aja,
675 | // gak pake lama.
676 | $fields['Search_Option'] = 'TxnPrd';
677 | switch ($this->range) {
678 | case 'now':
679 | case 'today':
680 | $fields['TxnPeriod'] = 'Today';
681 | break;
682 |
683 | case 'last week':
684 | $fields['TxnPeriod'] = 'LastWeek';
685 | break;
686 |
687 | case 'last month':
688 | $fields['TxnPeriod'] = 'LastMonth';
689 | break;
690 |
691 | default:
692 | $fields['TxnPeriod'] = '-1';
693 | $fields['Search_Option'] = 'Date';
694 |
695 | // Aturan BNI adalah sekali request, maka total maksimal
696 | // 31 hari, berarti antara start dan end ada 30 hari.
697 | if ($this->range->isSameMonth() || $this->range->diff()->days <= 30) {
698 | // tidak perlu dipecah.
699 | $fields['txnSrcFromDate'] = $this->range->format(self::BNI_DATE_FORMAT, 'start');
700 | $fields['txnSrcToDate'] = $this->range->format(self::BNI_DATE_FORMAT, 'end');
701 | }
702 | else {
703 | // Untuk interval lebih dari 30 hari, maka kita perlu
704 | // pecah menjadi per bulan.
705 | $this->configuration('temporary][over_range', true);
706 | $this->range = $this->range->splitPerMonth();
707 | // Hasil split per month adalah asc, maka sesuaikan
708 | if ($this->sort == 'desc') {
709 | krsort($this->range);
710 | }
711 |
712 | $first = array_shift($this->range);
713 | $fields['txnSrcFromDate'] = $first->format(self::BNI_DATE_FORMAT, 'start');
714 | $fields['txnSrcToDate'] = $first->format(self::BNI_DATE_FORMAT, 'end');
715 | }
716 | break;
717 | }
718 | $this->configuration('menu][bni_select_range_form][url', $url);
719 | $this->configuration('menu][bni_select_range_form][fields', $fields);
720 | break;
721 | }
722 | }
723 |
724 | /**
725 | * Alternative handler for bni_parse_transaction_page.
726 | */
727 | protected function bniParseTransactionPage()
728 | {
729 | switch ($this->target) {
730 | case 'get_range_transaction':
731 | $tables = $this->html->extractTable(true);
732 | $transaction = $this->bniFilterTransactionTable($tables);
733 | $temporary_result = $this->configuration('temporary][result');
734 | if (null === $temporary_result) {
735 | $temporary_result = [];
736 | }
737 | $temporary_result = array_merge($temporary_result, $transaction);
738 | $this->configuration('temporary][result', $temporary_result);
739 | break;
740 | }
741 | }
742 |
743 | /**
744 | * Untuk range yang lebih dari 31 hari, maka perlu dilakukan split.
745 | * Alternative handler for
746 | * bni_parse_if_over_range_then_save_select_range_page_location.
747 | */
748 | protected function bniParseIfOverRangeThenSaveSelectRangePageLocation()
749 | {
750 | $is_over_range = $this->configuration('temporary][over_range');
751 | if ($is_over_range) {
752 | // Cek lagi apakah sudah selesai loopingnya.
753 | // looping akan dikurangi oleh handler
754 | // bni_parse_select_range_page_revisited
755 | if (empty($this->range)) {
756 | // Hapus informasi over_range.
757 | $this->configuration('temporary][over_range', false);
758 | return;
759 | }
760 |
761 | $form = $this->html->find('form');
762 | $url = $form->attr('action');
763 |
764 | $fields = $form->preparePostForm('__BACK__');
765 | $this->configuration('menu][bni_select_range_page][url', $url);
766 | $this->configuration('menu][bni_select_range_page][fields', $fields);
767 | }
768 | }
769 |
770 | /**
771 | * Alternative handler for
772 | * bni_parse_if_has_next_page_then_visit_transaction_next_page_prepend.
773 | */
774 | protected function bniParseIfHasNextPageThenVisitTransactionNextPagePrepend()
775 | {
776 | // cari link berikutnya
777 | try {
778 | $url_raw = $this->html->find('a#NextData')->attr('href');
779 | if (null === $url_raw) {
780 | throw new \Exception;
781 | }
782 | preg_match('/^javascript\:fnCallAJAX\(\'(.*)\'\)$/', $url_raw, $m);
783 | if (empty($m) || !array_key_exists(1, $m)) {
784 | throw new \Exception;
785 | }
786 | $url = $m[1];
787 | $this->addStepFromReference('transaction_next_page');
788 | $this->configuration('menu][bni_transaction_next_page][url', $url);
789 | }
790 | catch (\Exception $e) {
791 | // Stop.
792 | }
793 | }
794 |
795 | /**
796 | * Alternative handler for
797 | * bni_parse_if_over_range_then_visit_select_range_page_append.
798 | */
799 | protected function bniParseIfOverRangeThenVisitSelectRangePageAppend()
800 | {
801 | $is_over_range = $this->configuration('temporary][over_range');
802 | if ($is_over_range) {
803 | $this->addStepFromReference('revisit_select_range_page');
804 | }
805 | }
806 |
807 | /**
808 | * Alternative handler for: bni_parse_select_range_page_revisited
809 | */
810 | protected function bniParseSelectRangePageRevisited()
811 | {
812 | $next = array_shift($this->range);
813 | $form = $this->html->find('form');
814 | $url = $form->attr('action');
815 | $fields = $form->preparePostForm('FullStmtInqRq');
816 | $fields['TxnPeriod'] = '-1';
817 | $fields['Search_Option'] = 'Date';
818 | $fields['txnSrcFromDate'] = $next->format(self::BNI_DATE_FORMAT, 'start');
819 | $fields['txnSrcToDate'] = $next->format(self::BNI_DATE_FORMAT, 'end');
820 | $this->addStepFromReference('revisit_select_range_form');
821 | $this->configuration('menu][bni_select_range_form][url', $url);
822 | $this->configuration('menu][bni_select_range_form][fields', $fields);
823 |
824 | }
825 |
826 | /**
827 | * Mendapatkan array transaksi dengan format yang sudah rapih.
828 | */
829 | protected function bniFilterTransactionTable($tables)
830 | {
831 | $ref = IBank::reference('table_header_account_statement');
832 | $ref = array_flip($ref);
833 |
834 | $language = $this->configuration('language');
835 | $language = null === $language ? 'id' : $language;
836 |
837 | $transactions = [];
838 | while (!empty($tables)) {
839 | $table = array_shift($tables);
840 |
841 | if (isset($table[0]) && isset($table[1])) {
842 | $info = $language == 'id' ? $this->bniTranslate($table[0]) : $table[0];
843 | $value = $table[1];
844 | switch ($info) {
845 | case 'Transaction Date':
846 | $transaction = ['no' => null, 'id' => null];
847 | $transaction['date'] = $value;
848 | break;
849 |
850 | case 'Transaction Remarks':
851 | $transaction['detail'] = $value;
852 | break;
853 |
854 | case 'Amount type':
855 | $transaction['type'] = $value;
856 | break;
857 |
858 | case 'Amount':
859 | $transaction['amount'] = $value;
860 | break;
861 |
862 | case 'Account Balance':
863 | $transaction['balance'] = $value;
864 | $transactions[] = array_merge($ref, $transaction);
865 | break;
866 | }
867 | }
868 | }
869 | return $transactions;
870 | }
871 |
872 | /**
873 | * Alternative handler for bni_transaction_finishing.
874 | */
875 | protected function bniTransactionFinishing()
876 | {
877 | $temporary_result = $this->configuration('temporary][result');
878 | $this->configuration('temporary][result', null);
879 |
880 | // Secara default, bni menggunakan sort secara desc.
881 | switch ($this->sort) {
882 | case 'desc':
883 | break;
884 |
885 | case 'asc':
886 | krsort($temporary_result);
887 | break;
888 | }
889 | if (null === $this->result) {
890 | $this->result = [];
891 | }
892 | $this->result = array_merge($this->result, $temporary_result);
893 | }
894 |
895 | /**
896 | * Translate dari indonesia ke inggiris.
897 | */
898 | protected function bniTranslate($string)
899 | {
900 | return isset($this->bniString()[$string]) ? $this->bniString()[$string] : $string;
901 | }
902 |
903 | /**
904 | * Kamus.
905 | */
906 | protected function bniString()
907 | {
908 | return [
909 | 'Tanggal Transaksi' => 'Transaction Date',
910 | 'Uraian Transaksi' => 'Transaction Remarks',
911 | 'Tipe' => 'Amount type',
912 | 'Jumlah Pembayaran' => 'Amount',
913 | 'Nominal' => 'Amount',
914 | 'Saldo' => 'Account Balance',
915 | ];
916 | }
917 |
918 | protected function bniCheckRange()
919 | {
920 | if (null === $this->range) {
921 | $this->changeTarget('get_last_transaction');
922 | }
923 | else {
924 | $this->changeTarget('get_range_transaction');
925 | }
926 | }
927 |
928 |
929 |
930 |
931 |
932 |
933 | }
--------------------------------------------------------------------------------