File Coverage

blib/lib/Finance/Bank/DE/DeutscheBank.pm
Criterion Covered Total %
statement 37 268 13.8
branch 0 90 0.0
condition 0 50 0.0
subroutine 13 31 41.9
pod 5 17 29.4
total 55 456 12.0


/sm || $content =~ /
/ || $content =~ /
/ ;
line stmt bran cond sub pod time code
1             package Finance::Bank::DE::DeutscheBank;
2 1     1   9933 use strict;
  1         2  
  1         51  
3 1     1   21 use warnings;
  1         2  
  1         40  
4 1     1   5 use Carp;
  1         8  
  1         88  
5 1     1   6 use base 'Class::Accessor';
  1         2  
  1         2632  
6              
7 1     1   4495 use WWW::Mechanize;
  1         493090  
  1         45  
8 1     1   9673 use HTML::LinkExtractor;
  1         2793  
  1         35  
9 1     1   1420 use HTML::TreeBuilder;
  1         69276  
  1         17  
10 1     1   1649 use Text::CSV_XS;
  1         11868  
  1         81  
11              
12              
13 1     1   12 use vars qw[ $VERSION ];
  1         4  
  1         71  
14              
15             $VERSION = '0.06';
16              
17             BEGIN {
18 1     1   16 Finance::Bank::DE::DeutscheBank->mk_accessors(qw( agent ));
19             };
20              
21 1     1   107 use constant BASEURL => 'https://meine.deutsche-bank.de';
  1         2  
  1         77  
22 1     1   6 use constant LOGIN => BASEURL . '/trxm/db/';
  1         1  
  1         61  
23 1     1   5 use constant FUNCTIONS => "(Übersicht)|(Ihr Konto)|(Ihr Depot)|(Service / Optionen)|(Umsatzanzeige)|(Inlands-Überweisung)|(Daueraufträge)|(Lastschrift)|(Kunden-Logout)|(Überweisungsvorlagen)|(Ihre Finanzübersicht als PDF-Datei speichern)|(Ihre Finanzübersicht als CSV-Datei speichern)|(Customer-Logout)|(Overview)|(Your account)|(Your sec. account)|(Service / Options)|(Transactions)|(Domestic transfer order)|(Standing orders)|(Direct debit)|(Transfer order templates)|(Customer-Logout)|(Save as PDF File)|(Save as CSV File)|(Save your financial overview as PDF file)|(Save your financial overview as CSV file)";
  1         3  
  1         3356  
24              
25              
26             sub new
27             {
28 0     0 1   my ($class,%args) = @_;
29              
30 0 0         croak "Filiale/Branch number must be specified"
31             unless $args{Branch};
32              
33 0 0         croak "Konto/Account number must be specified"
34             unless $args{Account};
35              
36 0 0         croak "Unterkonto/SubAccount number must be specified"
37             unless $args{SubAccount};
38              
39 0 0         croak "PIN/Password must be specified"
40             unless $args{PIN};
41              
42 0   0 0     my $logger = $args{status} || sub {};
  0            
43              
44 0           my $self = {
45             agent => undef,
46             account => $args{Account},
47             password => $args{PIN},
48             branch => $args{Branch},
49             subaccount => $args{SubAccount},
50             logger => $logger,
51             navigation => undef,
52             };
53 0           bless $self, $class;
54              
55 0           $self->log("New $class created");
56 0           $self;
57             };
58              
59              
60             sub log
61             {
62 0     0 0   $_[0]->{logger}->(@_);
63             };
64              
65              
66             sub log_httpresult
67             {
68 0     0 0   $_[0]->log("HTTP Code",$_[0]->agent->status,$_[0]->agent->res->as_string)
69             };
70              
71              
72             sub new_session
73             {
74             # Reset our user agent
75 0     0 0   my ($self) = @_;
76 0           my $url;
77              
78 0 0         $self->close_session()
79             if ($self->agent);
80              
81 0           my $result = $self->get_login_page(LOGIN);
82              
83 0 0         if ( $result != 200 )
84             {
85 0           $self->log("Status"," Banking is unavailable");
86 0           die "Banking is unavailable";
87             }
88              
89 0 0         if ( $result == 200 )
90             {
91 0 0         if ($self->maintenance)
92             {
93 0           $self->log("Status","Banking is unavailable due to maintenance");
94 0           die "Banking unavailable due to maintenance";
95             };
96              
97 0           my $agent = $self->agent();
98 0           my $function = 'ACCOUNTBALANCE';
99 0           $self->log("Logging into function $function");
100              
101             # gvo=DisplayFinancialOverview&loginTab=iTAN&javascriptEnabled=true&branch=XXX&account=XXXXXX&subaccount=XX&pin=XXXXX&quickLink=DisplayFinancialOverview
102 0           $agent->current_form->value('gvo','DisplayFinancialOverview');
103 0           $agent->current_form->value('loginTab','iTAN');
104 0           $agent->current_form->value('javascriptEnabled','false');
105 0           $agent->current_form->value('quickLink','DisplayFinancialOverview');
106 0           $agent->current_form->value('account',$self->{account});
107 0           $agent->current_form->value('branch',$self->{branch});
108 0           $agent->current_form->value('subaccount',$self->{subaccount});
109 0           $agent->current_form->value('pin',$self->{password});
110 0           $agent->add_header('Accept-Charset' => 'utf-8');
111 0           $agent->add_header('Accept-Encoding' => '');
112              
113 0           local $^W=0;
114 0           $result = $agent->submit();
115              
116 0 0         if ( $self->access_denied )
117             {
118 0           $self->log("Not possible to authenticate at bank server ( wrong account/pin combination ? )");
119 0           return 0;
120             }
121              
122             # extract links to account functions
123 0           my $LinkExtractor = new HTML::LinkExtractor();
124              
125 0           $LinkExtractor->strip( 1 );
126 0           $LinkExtractor->parse(\$agent->content());
127              
128             # needed here because of empty links ( href attribute )
129 0           local $^W=1;
130              
131             # now we have the links in the format
132             # {
133             # '_TEXT' => 'Overview',
134             # 'target' => '_top',
135             # 'href' => '/mod/WebObjects/dbpbc.woa/618/wo/HpRl1hqezkxfYRosJRjTg0/4.11.1.5.3.3.5.3',
136             # 'tag' => 'a',
137             # 'class' => 'NaviDirektLink'
138             # },
139             # { ...
140             # }
141             #
142              
143              
144             # but I would like to have them as
145             # {
146             # '_TEXT' => 'Overview',
147             # 'href' => '/mod/WebObjects/dbpbc.woa/618/wo/HpRl1hqezkxfYRosJRjTg0/4.11.1.5.3.3.5.3',
148             # },
149             # { ...
150             # }
151             # and only for functions ( not all links and images ... )
152              
153 0           my @tmp = ();
154 0           foreach my $elem ( @{$LinkExtractor->links} )
  0            
155             {
156 0 0 0       if (( defined( $elem->{ '_TEXT' } ) && ( $elem->{ '_TEXT' } ne '' ) && ( $elem->{ '_TEXT' } =~ "m/". FUNCTIONS ."/" )) )
      0        
157             {
158 0           foreach $_ ( keys %$elem )
159             {
160 0 0         if ( $_ !~ m/(_TEXT)|(href)/ )
161             {
162 0           delete $elem->{ $_ };
163             }
164             }
165              
166 0           push @tmp, \%$elem;
167             }
168             }
169              
170             # save these links so that we can remember them
171 0           $self->{navigation} = \@tmp;
172 0           $self->log_httpresult();
173 0           $result = $agent->status;
174             };
175 0           $result;
176             };
177              
178              
179             sub get_login_page
180             {
181 0     0 0   my ($self,$url) = @_;
182 0           $self->log("Connecting to $url");
183 0           $self->agent(WWW::Mechanize->new(agent => "Mozilla/4.78 (Linux 2.4.19-4GB i686; U) Opera 6.03 [en]", cookie_jar => {} ));
184              
185 0           my $agent = $self->agent();
186 0           $agent->add_header('Accept-Language' => 'en-us,en;q=0.5');
187 0           $agent->get(LOGIN);
188 0           $self->log_httpresult();
189 0           $agent->status;
190             };
191              
192              
193             sub error_page
194             {
195             # Check if an error page is shown (a page with much red on it)
196 0     0 0   my ($self) = @_;
197 0           my $content = $self->agent->content;
198 0           $content =~ s!
!!;
199 0           $content =~ s!
!!;
200 0 0 0       $content =~ /
201             };
202              
203              
204             sub maintenance
205             {
206 0     0 0   my ($self) = @_;
207              
208             # would be nice if someone could mail me the actual english and german messages which are displayed in case of maintenance ...
209 0 0         $self->error_page or
210             $self->agent->content =~ /derzeit steht das Internet Banking aufgrund von Wartungsarbeiten leider nicht zur Verfügung.\s*
\s*In Kürze wird das Internet Banking wieder wie gewohnt erreichbar sein./gsm;
211             };
212              
213              
214             sub access_denied
215             {
216 0     0 0   my ($self) = @_;
217 0           my $content = $self->agent->content;
218              
219 0 0 0       $self->error_page or
      0        
      0        
      0        
      0        
      0        
      0        
220             ( $content =~ /Die eingegebene Kontonummer ist unvollständig oder falsch\..*\(2051\)/gsm
221             or $content =~ /Die eingegebene PIN ist falsch\. Bitte geben Sie die richtige PIN ein\.\s*\(10011\)/gsm
222             or $content =~ /Die von Ihnen eingegebene Kontonummer ist ungültig und entspricht keiner Deutsche Bank-Kontonummer.\s*\(3040\)/gsm
223             or $content =~ /Leider konnte Ihre Anmeldung nicht erfolgreich durchgeführt werden/
224             or $content =~ /Unfortunately, you were not able to register successfully./
225             or $content =~ /Bitte überprüfen Sie Ihre Anmeldedaten oder versuchen Sie es zu einem späteren Zeitpunkt noch einmal./
226             or $content =~ /Please check your registration data or try again later./
227             or $content =~ /Bitte geben Sie ein gültiges Datum ein/
228             );
229             };
230              
231              
232             sub session_timed_out
233             {
234 0     0 0   my ($self) = @_;
235 0           $self->agent->content =~ /Die Sitzungsdaten sind ungültig, bitte führen Sie einen erneuten Login durch.\s+\(27000\)/;
236             };
237              
238              
239             sub functions
240             {
241 0     0 0   my ($self,$function) = @_;
242 0           my $link = ();
243              
244 0 0         if ( $function =~ "m/". FUNCTIONS ."/" )
245             {
246 0           foreach $_ ( @{$self->{ navigation }} )
  0            
247             {
248 0 0         if ( $_->{ '_TEXT' } eq $function )
249             {
250 0           $link = $_->{ 'href' };
251             }
252             }
253 0           return $link;
254             }
255             else
256             {
257 0           return 0;
258             }
259             }
260              
261              
262             sub select_function
263             {
264 0     0 1   my ($self,$function) = @_;
265 0 0         carp "Unknown account function '$function'"
266             unless $self->functions($function);
267              
268 0 0         $self->new_session unless $self->agent;
269 0           $self->agent->get( $self->functions( "$function" ) );
270              
271 0 0         if ( $self->session_timed_out )
272             {
273 0           $self->log("Session timed out");
274 0           $self->agent(undef);
275 0           $self->new_session();
276 0           $self->agent->get( $self->functions( $function ) );
277             };
278 0           $self->log_httpresult();
279 0           $self->agent->status;
280             };
281              
282              
283             sub close_session
284             {
285 0     0 1   my ($self) = @_;
286 0           my $result;
287 0 0         if (not $self->access_denied)
288             {
289 0           $self->log("Closing session");
290 0           local $^W=0;
291 0           $self->select_function('Customer-Logout');
292 0           local $^W=1;
293 0           $result = $self->agent->res->as_string =~ /https:\/\/meine.deutsche-bank.de\/trxm\/db\/.*;link=trxm_en_logout-pbcde-to-txm_login.*/;
294             }
295             else
296             {
297 0           $result = 'Never logged in';
298             };
299 0           $self->agent(undef);
300 0           $result;
301             };
302              
303              
304             sub login
305             {
306 0     0 1   my ($self) = @_;
307              
308 0 0         if ( $self->new_session() )
309             {
310 0           return 1;
311             }
312             else
313             {
314 0           return 0;
315             }
316              
317             };
318              
319              
320             sub parse_account_overview
321             {
322 0     0 0   my ($self) = @_;
323 0           my $agent = $self->agent();
324 0           my %saldo = ();
325              
326 0           my $tree = HTML::TreeBuilder->new();
327 0           $tree->parse( $agent->content() );
328              
329 0           foreach my $table ( $tree->look_down('_tag', 'table') )
330             {
331 0           foreach my $row ( $table->look_down('_tag', 'tr') )
332             {
333 0           foreach my $child ( $row->look_down('_tag', 'td') )
334             {
335 0 0 0       if (( defined $child->attr('class')) && ( $child->attr('class') eq 'balance'))
336             {
337 0           my $tmp = $child->as_trimmed_text;
338              
339 0 0         if ( $child->attr('class') eq 'balance')
340             {
341 0 0         if ( $tmp =~ m/\.[0-9][0-9]$/ )
342             {
343 0           $tmp =~ s/\./#/;
344 0           $tmp =~ s/,/./g;
345 0           $tmp =~ s/#/,/;
346             }
347 0           $saldo{ 'Saldo' } = $tmp;
348             }
349             }
350              
351 0           foreach my $morechildren ( $child->look_down('_tag', 'acronym') )
352             {
353 0 0 0       if (( defined $morechildren->attr('title')) && ( $morechildren->attr('title') eq 'Euro'))
354             {
355 0           $saldo{ 'Währung' } = $morechildren->as_trimmed_text;
356             }
357             }
358             }
359             }
360             }
361 0           return %saldo
362             }
363              
364              
365             sub saldo
366             {
367 0     0 0   my ($self) = @_;
368 0           my $agent = $self->agent;
369 0 0         if ($agent)
370             {
371 0           local $^W=0;
372 0           $self->select_function('Overview');
373 0           local $^W=1;
374 0           return $self->parse_account_overview();
375             }
376             else
377             {
378 0           return undef;
379             }
380             };
381              
382              
383             sub MapData
384             {
385 0     0 0   my ( @data ) = @_;
386 0           for my $row ( @data )
387             {
388 0           foreach $_ ( keys %$row )
389             {
390 0 0 0       if (( $_ eq 'Haben' )||( $_ eq 'Soll' ))
    0 0        
    0          
391             {
392 0           $row->{ $_ } =~ s/\./#/;
393 0           $row->{ $_ } =~ s/,/./g;
394 0           $row->{ $_ } =~ s/#/,/;
395             }
396             elsif (( $_ eq 'Buchungstag' )||( $_ eq 'Wert' ))
397             {
398 0           my @tmp = split( /\//, $row->{ $_ } );
399 0           $row->{ $_ } = join( '.', $tmp[1], $tmp[0], $tmp[2] );
400             }
401             elsif ( $_ eq 'Waehrung' )
402             {
403 0           my $tmp = $row->{ 'Waehrung' };
404 0           delete $row->{ 'Waehrung' };
405 0           $row->{ 'Währung' } = $tmp;
406             }
407             }
408             }
409 0           return @data;
410             }
411              
412              
413             sub account_statement
414             {
415 0     0 1   my ($self, %parameter) = @_;
416              
417 0           my $count = 0;
418 0           my @header = ();
419 0           my @AccountStatement = ();
420 0           my $AccountRow = ();
421 0           my @date;
422 0           my $agent = $self->agent;
423 0 0         if ($agent)
424             {
425 0           local $^W=0;
426 0           $self->select_function('Overview');
427              
428 0           my %account = $self->parse_account_overview();
429 0           $agent->follow_link( 'text' => 'Transactions' );
430 0           local $^W=1;
431              
432             # should I get account statement for user defined period ?
433 0 0         if ( defined $parameter{ 'period' } )
    0          
434             {
435 0           my ( $day, $month, $year ) = split( '\.', $parameter{ 'StartDate' } );
436              
437 0           $day = sprintf("%02d", $day );
438 0           $month = sprintf("%02d", $month );
439 0           $year = sprintf("%04d", $year );
440              
441 0 0         croak "Year must have 4 digits in StartDate"
442             unless ( length $year == 4 );
443              
444 0           $agent->current_form->value( 'period','dynamicRange');
445 0           $agent->current_form->value( 'periodStartDay', $day );
446 0           $agent->current_form->value( 'periodStartMonth', $month );
447 0           $agent->current_form->value( 'periodStartYear', $year );
448              
449 0           ( $day, $month, $year ) = split( '\.', $parameter{ 'EndDate' } );
450 0           $day = sprintf("%02d", $day );
451 0           $month = sprintf("%02d", $month );
452 0           $year = sprintf("%04d", $year );
453              
454 0 0 0       croak "Year must have 4 digits in EndDate"
455             unless (( length $year == 4 )&&( $year > 1900 ));
456              
457 0           $agent->current_form->value( 'period','dynamicRange');
458 0           $agent->current_form->value( 'periodEndDay', $day );
459 0           $agent->current_form->value( 'periodEndMonth', $month );
460 0           $agent->current_form->value( 'periodEndYear', $year );
461             }
462             elsif ( defined $parameter{ 'last' } )
463             {
464 0           my $last = ();
465 0           $agent->current_form->value('period','fixedRange');
466 0 0         if ( $parameter{ 'last' } <= 10 )
    0          
    0          
    0          
    0          
    0          
467             {
468 0           $last = 10;
469             }
470             elsif ( $parameter{ 'last' } <= 20 )
471             {
472 0           $last = 20;
473             }
474             elsif ( $parameter{ 'last' } <= 30 )
475             {
476 0           $last = 30;
477             }
478             elsif ( $parameter{ 'last' } <= 60 )
479             {
480 0           $last = 60;
481             }
482             elsif ( $parameter{ 'last' } <= 90 )
483             {
484 0           $last = 90;
485             }
486             elsif ( $parameter{ 'last' } <= 120 )
487             {
488 0           $last = 120;
489             }
490             else # > 120
491             {
492 0           $last = 180;
493             }
494 0           $agent->select('periodDays', $last);
495             }
496             else #expect that per default last login date is set ...
497             {
498             ;
499             }
500              
501 0           local $^W=0;
502             # 'refresh view' is used to trigger update of account balance
503 0           my $result = $agent->submit();
504              
505             # download CSV formated data of account balances
506 0           $result = $agent->follow_link( text_regex => qr/Save your account turnover as.*CSV.*file/ );
507 0           local $^W=1;
508              
509             #successfully downloaded account balance data in csv format
510 0 0         if ( $result->is_success )
511             {
512 0           my @balance = split( '\n', $result->content );
513 0           my $csv = Text::CSV_XS->new( { 'sep_char' => ';' });
514              
515 0           my $StartLineDetected = 0;
516 0           for ( my $loop = 0; $loop < scalar @balance; $loop++ )
517             {
518 0           my $line = $balance[ $loop ];
519 0           chomp( $line );
520              
521 0           $line =~ s/^Booking date;/Buchungstag;/;
522 0           $line =~ s/;Value date;/;Wert;/;
523 0           $line =~ s/;Transactions Payment details;/;Verwendungszweck;/;
524 0           $line =~ s/;Debit;/;Soll;/;
525 0           $line =~ s/;Credit;/;Haben;/;
526 0           $line =~ s/;Currency/;Waehrung/;
527 0           $line =~ s/^Account balance;/Kontostand;/;
528              
529 0 0 0       if ( $StartLineDetected == 1 )
    0          
    0          
530             {
531 0           my $status = $csv->parse( $line );
532 0           @header = $csv->fields();
533 0           $AccountRow = 0;
534 0           @AccountStatement = ();
535 0           $StartLineDetected = 2;
536             }
537             elsif ( $StartLineDetected == 2 )
538             {
539 0 0         if ( $line !~ /^Kontostand;/ )
540             {
541 0           my $status = $csv->parse( $line );
542 0           my @columns = $csv->fields();
543              
544 0           for (my $loop = 0; $loop < scalar @columns; $loop++ )
545             {
546 0           $AccountStatement[ $AccountRow ]{ $header[ $loop ] } = $columns[ $loop ];
547             }
548 0           $AccountRow++;
549             }
550             }
551             elsif ( ( $line =~ /Vorgemerkte und noch nicht gebuchte Umsätze sind nicht Bestandteil dieser Aufstellung/ )||
552             ( $line =~ /Transactions pending are not included in this report/ ) )
553             {
554 0           $StartLineDetected = 1;
555             }
556             }
557 0           return MapData( @AccountStatement );
558             }
559             else
560             {
561 0           return undef;
562             }
563             }
564             else
565             {
566 0           return undef;
567             }
568             }
569              
570              
571             1;
572             __END__