File Coverage

blib/lib/Finance/Bank/Postbank_de.pm
Criterion Covered Total %
statement 79 96 82.2
branch 12 18 66.6
condition 6 9 66.6
subroutine 22 31 70.9
pod 8 18 44.4
total 127 172 73.8


line stmt bran cond sub pod time code
1             package Finance::Bank::Postbank_de;
2              
3 9     9   536713 use 5.006; # we use lexical filehandles now
  9         56  
4 9     9   46 use strict;
  9         15  
  9         190  
5 9     9   37 use warnings;
  9         13  
  9         234  
6 9     9   41 use Carp;
  9         18  
  9         483  
7 9     9   3948 use Moo 2;
  9         83604  
  9         45  
8              
9 9     9   13705 use Time::Local;
  9         12355  
  9         447  
10 9     9   3723 use POSIX 'strftime';
  9         46135  
  9         39  
11              
12 9     9   14133 use Finance::Bank::Postbank_de::Account;
  9         26  
  9         261  
13 9     9   3503 use Finance::Bank::Postbank_de::APIv1;
  9         29  
  9         328  
14 9     9   60 use Encode qw(decode);
  9         15  
  9         788  
15 9     9   54 use Mozilla::CA;
  9         13  
  9         1946  
16              
17             #use IO::Socket::SSL qw(SSL_VERIFY_PEER SSL_VERIFY_NONE);
18              
19             our $VERSION = '0.56';
20              
21             has 'login' => (
22             is => 'ro',
23             );
24              
25             has 'password' => (
26             is => 'ro',
27             );
28              
29             has 'urls' => (
30             is => 'ro',
31             default => sub { {} },
32             );
33              
34             has 'logger' => (
35             is => 'ro',
36             default => sub { {} },
37             );
38              
39             has 'past_days' => (
40             is => 'ro',
41             default => sub { {} },
42             );
43              
44             has 'api' => (
45             is => 'rw',
46             default => sub {
47             my $api = Finance::Bank::Postbank_de::APIv1->new();
48             $api->configure_ua();
49             $api
50             },
51             );
52              
53             has 'session' => (
54             is => 'rw',
55             );
56              
57             has '_account_numbers' => (
58             is => 'rw',
59             );
60              
61             our %functions;
62             BEGIN {
63 9     9   8454 %functions = (
64             quit => [ text_regex => qr'\bBanking\s+beenden\b' ],
65             accountstatement => [ text_regex => qr'\bUms.*?tze\b' ],
66             );
67             };
68              
69             around BUILDARGS => sub {
70             my ($orig,$class,%args) = @_;
71              
72             croak "Login/Account number must be specified"
73             unless $args{login};
74             croak "Password/PIN must be specified"
75             unless $args{password};
76             if( exists $args{ status }) {
77             $args{ logger } = delete $args{ status };
78             };
79              
80             $orig->($class, %args);
81             };
82              
83 0     0 0 0 sub log { $_[0]->logger->(@_); };
84 0     0 0 0 sub log_httpresult { $_[0]->log("HTTP Code",$_[0]->agent->status,$_[0]->agent->res->headers->as_string . $_[0]->agent->content) };
85              
86             sub new_session {
87 7     7 1 728 my ($self) = @_;
88              
89 7 50       44 $self->close_session()
90             if ($self->session);
91 7         16 my $pb;
92 7         14 my $ok = eval {
93 7         55 $pb = $self->api->login( $self->login, $self->password );
94 4         7091 1
95             };
96 7 100       120 if( ! $ok ) {
97             #warn sprintf "Got HTTP error %d, message %s", $self->api->ua->status, $self->api->ua->message;
98             #croak $@;
99             } else {
100 4         33 $self->session( $pb );
101             }
102             };
103              
104             sub is_security_advice {
105 0     0 0 0 my ($self) = @_;
106             #$self->agent->content() =~ /\bZum\s+Finanzstatus\b/;
107             };
108              
109             sub is_nutzungshinweis {
110 0     0 0 0 my ($self) = @_;
111             #$self->agent->content() =~ /\bAus Sicherheitsgr.*?nden haben wir einige\b/;
112             };
113              
114              
115             sub skip_security_advice {
116 0     0 0 0 my ($self) = @_;
117             #$self->log('Skipping security advice page');
118             #$self->agent->follow_link(text_regex => qr/\bZum\s+Finanzstatus\b/);
119             # $self->agent->content() =~ /Sicherheitshinweis/;
120             };
121              
122             sub skip_nutzungshinweis {
123 0     0 0 0 my ($self) = @_;
124             #$self->log('Skipping nutzungshinweis page');
125             #$self->agent->follow_link(text_regex => qr/\bZur\s+Konten.bersicht\b/);
126             # $self->agent->content() =~ /Sicherheitshinweis/;
127             };
128              
129             sub error_page {
130             # Check if an error page is shown
131 3     3 0 19 my ($self) = @_;
132 3         23 $self->api->ua->status != 200
133             #return unless $self->agent;
134             #
135             #$self->agent->content =~ m!<p\s+class="form-error">!sm
136             # or
137             #$self->agent->content =~ m!<p\s+class="field-error">!sm
138             # or $self->maintenance;
139             };
140              
141             sub error_message {
142 0     0 0 0 my ($self) = @_;
143             #return unless $self->agent;
144             #die "No error condition detected in:\n" . $self->agent->content
145             # unless $self->error_page;
146             #if(
147             #$self->agent->content =~ m!<p\s+class="form-error">\s*<strong>\s*(.*?)\s*</strong>\s*</p>!sm
148             # or
149             #$self->agent->content =~ m!<p\s+class="field-error">\s*(.*?)\s*</p>!sm
150             # ) { return $1 }
151             # #or croak "No error message found in:\n" . $self->agent->content;
152 0         0 return ''
153             };
154              
155             sub maintenance {
156 1     1 1 18 my ($self) = @_;
157             #return unless $self->agent;
158             ##$self->error_page and
159             #$self->agent->content =~ m!Sehr geehrter <span lang="en">Online-Banking</span>\s+Nutzer,\s+wegen einer hohen Auslastung kommt es derzeit im Online-Banking zu\s*l&auml;ngeren Wartezeiten.!sm
160             #or $self->agent->content =~ m!&nbsp;Wartung\b!
161             #or $self->agent->content =~ m!<p class="important">\s*<strong>\s*Diese Funktion steht auf Grund einer technischen St.*?rung derzeit leider nicht zur Verf.*?gung.*?</strong>\s*</p>!sm # Testumgebung...
162             ()
163 1         2 };
164              
165             sub access_denied {
166 3     3 0 1784 my ($self) = @_;
167 3         17 $self->api->ua->status == 401
168             #if ($self->error_page) {
169             # my $message = $self->error_message;
170             #
171             # return (
172             # $message =~ m!^Die Kontonummer ist nicht für das Internet Online-Banking freigeschaltet. Bitte verwenden Sie zur Freischaltung den Link "Online-Banking freischalten"\.<br />\s*$!sm
173             # or $message =~ m!^Sie haben zu viele Zeichen in das Feld eingegeben.<br />\s*$!sm
174             # or $message =~ m!^Die eingegebene Postbank Girokontonummer ist zu lang. Bitte überprüfen Sie Ihre Eingabe.$!sm
175             # or $message =~ m!^Die Anmeldung ist fehlgeschlagen. Bitte vergewissern Sie sich der Richtigkeit Ihrer Eingaben und f.*?hren Sie den Anmeldevorgang erneut durch.\s*$!sm
176             # )
177             #} else {
178             # return;
179             #};
180             };
181              
182             sub session_timed_out {
183 0     0 1 0 my ($self) = @_;
184             #$self->agent->content =~ /Die Sitzungsdaten sind ung&uuml;ltig, bitte f&uuml;hren Sie einen erneuten Login durch.\s+\(27000\)/;
185             ()
186 0         0 };
187              
188             sub select_function {
189 0     0 1 0 my ($self,$function) = @_;
190 0 0       0 if (! $self->session) {
191 0         0 $self->new_session;
192             };
193             croak "Unknown account function '$function'"
194 0 0       0 unless exists $functions{$function};
195 0         0 my $method = $functions{ $function };
196              
197 0         0 my $res = $self->session->navigate($method);
198 0         0 $res
199             };
200              
201             sub close_session {
202 2     2 1 2562 my ($self) = @_;
203 2         22 $self->session(undef);
204 2         188 $self->api(undef);
205 2         1137 1
206             };
207              
208             sub finanzstatus {
209 3     3 0 8 my( $self ) = @_;
210 3 100       16 $self->new_session unless $self->session;
211 3         26 my $finanzstatus = $self->session->navigate(
212             class => 'Finance::Bank::Postbank_de::APIv1::Finanzstatus',
213             path => ['banking_v1' => 'financialstatus']
214             );
215             }
216              
217             sub _build_account_numbers {
218 2     2   6 my ($self,%args) = @_;
219            
220 2         7 my $finanzstatus = $self->finanzstatus;
221 2         91 (my $bp) = $finanzstatus->get_businesspartners; # always take the first...
222 2         107 my %numbers;
223             # this currently includes the credit card numbers ...
224 2         13 for my $acc ( $bp->get_accounts() ) {
225 14 100 100     106 $numbers{ $acc->iban } = $acc if ($acc->productType ne 'DEPOT' and $acc->productType ne 'BAUFINANZIERUNG');
226             };
227              
228 2         92 return $self->_account_numbers( \%numbers );
229             }
230              
231             sub account_numbers {
232 1     1 1 7 my ($self,%args) = @_;
233              
234 1   33     11 my $n = $self->_account_numbers || $self->_build_account_numbers;
235            
236 1         3 sort keys %{ $n };
  1         10  
237             };
238              
239             sub get_account_statement {
240 11     11 1 4267 my ($self,%args) = @_;
241              
242             #my $past_days = $args{past_days} || $self->{past_days};
243             #if($past_days) {
244             # my ($day, $month, $year) = split/\./, $agent->current_form->value('umsatzanzeigeGiro:salesForm:umsatzFilterOptionenAufklappbarSuchfeldPanel:accordion:vonBisDatum:datumForm:bisGruppe:bisDatum');
245             # my $end_epoch = timegm(0, 0, 0, $day, $month-1, $year);
246             # my $from_date = strftime '%d.%m.%Y', localtime($end_epoch-($past_days-1)*60*60*24);
247             # $agent->current_form->value('umsatzanzeigeGiro:salesForm:umsatzFilterOptionenAufklappbarSuchfeldPanel:accordion:vonBisDatum:datumForm:vonGruppe:vonDatum' => $from_date);
248             #};
249              
250 11   66     63 my $accounts = $self->_account_numbers || $self->_build_account_numbers;
251              
252 11 100       34 if( ! $args{ account_number }) {
253             # Hopefully we only got one account (?!)
254 1         4 ($args{ account_number }) = keys %$accounts;
255             };
256              
257 11         28 my $account = $accounts->{ $args{ account_number }};
258              
259             #if (exists $args{account_number}) {
260             # $self->log("Getting account statement for $args{account_number}");
261             # # Load the account numbers if not already loaded
262             # $self->account_numbers;
263             # if(! exists $self->{account_numbers}->{$args{account_number}}) {
264             # croak "Unknown account number '$args{account_number}'";
265             # };
266             # my $index = $self->{account_numbers}->{$args{account_number}};
267             # $agent->current_form->param( 'selectForm:kontoauswahl' => $index );
268             #} else {
269             # my @accounts = $agent->current_form->value('selectForm:kontoauswahl');
270             # $self->log("Getting account statement via default (@accounts)");
271             #};
272 11         49 my $content = $account->transactions_csv();
273 11 100       1254 if( $args{ file }) {
274             open my $fh, '>', $args{ file }
275 1 50       102 or croak "Couldn't create '$args{ file }': $!";
276 1         18 binmode $fh, ':encoding(UTF-8)';
277 1         232 print $fh $content;
278             };
279             #if ($agent->status == 200) {
280 11         34 my $result = $content;
281             # Result is in UTF-8
282 11         109 return Finance::Bank::Postbank_de::Account->parse_statement(content => $result);
283             };
284              
285             sub unread_messages {
286 1     1 1 9 my( $self )= @_;
287 1         6 $self->finanzstatus->available_messages
288             }
289              
290             1;
291             __END__
292              
293             =encoding ISO8859-1
294              
295             =head1 NAME
296              
297             Finance::Bank::Postbank_de - Check your Postbank.de bank account from Perl
298              
299             =head1 SYNOPSIS
300              
301             =for example begin
302              
303             use strict;
304             require Crypt::SSLeay; # It's a prerequisite
305             use Finance::Bank::Postbank_de;
306             my $account = Finance::Bank::Postbank_de->new(
307             login => 'Petra.Pfiffig',
308             password => '123456789',
309             status => sub { shift;
310             print join(" ", @_),"\n"
311             if ($_[0] eq "HTTP Code")
312             and ($_[1] != 200)
313             or ($_[0] ne "HTTP Code");
314              
315             },
316             );
317             # Retrieve account data :
318             my $retrieved_statement = $account->get_account_statement();
319             print "Statement date : ",$retrieved_statement->balance->[0],"\n";
320             print "Balance : ",$retrieved_statement->balance->[1]," EUR\n";
321              
322             # Output CSV for the transactions
323             for my $row ($retrieved_statement->transactions) {
324             print join( ";", map { $row->{$_} } (qw( tradedate valuedate type comment receiver sender amount ))),"\n";
325             };
326              
327             $account->close_session;
328             # See Finance::Bank::Postbank_de::Account for
329             # a simpler example
330              
331             =for example end
332              
333             =for example_testing
334             isa_ok($account,"Finance::Bank::Postbank_de");
335             isa_ok($retrieved_statement,"Finance::Bank::Postbank_de::Account");
336             $::_STDOUT_ =~ s!^Statement date : \d{8}\n!!m;
337             $::_STDOUT_ =~ s!^Skipping security advice page\n!!m;
338             my $expected = <<EOX;
339             New Finance::Bank::Postbank_de created
340             Connecting to https://banking.postbank.de/app/welcome.do
341             Activating (?-xism:^Kontoums.*?tze\$)
342             Getting account statement via default (9999999999)
343             Downloading text version
344             Statement date : ????????
345             Balance : 5314.05 EUR
346             .berweisung;111111/1000000000/37050198 FINANZKASSE 3991234 STEUERNUMMER 00703434;Finanzkasse K.ln-S.d;PETRA PFIFFIG;-328.75
347             .berweisung;111111/3299999999/20010020 .BERTRAG AUF SPARCARD 3299999999;Petra Pfiffig;PETRA PFIFFIG;-228.61
348             Gutschrift;BEZ.GE PERS.NR. 70600170/01 ARBEITGEBER U. CO;PETRA PFIFFIG;Petra Pfiffig;2780.70
349             .berweisung;DA 1000001;Verlagshaus Scribere GmbH;PETRA PFIFFIG;-31.50
350             Scheckeinreichung;EINGANG VORBEHALTEN GUTBUCHUNG 12345;PETRA PFIFFIG;Ein Fremder;1830.00
351             Lastschrift;MIETE 600+250 EUR OBJ22/328 SCHULSTR.7, 12345 MEINHEIM;Eigenheim KG;PETRA PFIFFIG;-850.00
352             Inh. Scheck;;2000123456789;PETRA PFIFFIG;-75.00
353             Lastschrift;TEILNEHMERNR 1234567 RUNDFUNK 0103-1203;GEZ;PETRA PFIFFIG;-84.75
354             Lastschrift;RECHNUNG 03121999;Telefon AG Köln;PETRA PFIFFIG;-125.80
355             Lastschrift;STROMKOSTEN KD.NR.1462347 JAHRESABRECHNUNG;Stadtwerke Musterstadt;PETRA PFIFFIG;-580.06
356             Gutschrift;KINDERGELD KINDERGELD-NR. 1462347;PETRA PFIFFIG;Arbeitsamt Bonn;154.00
357             Closing session
358             Activating (?-xism:^Banking beenden\$)
359             EOX
360             for ($::_STDOUT_,$expected) {
361             s!\r\n!\n!gsm;
362             s![\x80-\xff]!.!gsm;
363             # Strip out all date references ...
364             s/^\d{8};\d{8};//gm;
365             };
366             my @got = split /\n/, $::_STDOUT_;
367             my @expected = split /\n/, $expected;
368             is_deeply(\@got,\@expected,'Retrieving an account statement works')
369             or do {
370             diag "--- Got";
371             diag $::_STDOUT_;
372             diag "--- Expected";
373             diag $expected;
374             };
375              
376             =head1 DESCRIPTION
377              
378             This module provides a rudimentary interface to the Postbank online banking system at
379             https://meine.postbank.de/.
380              
381             The interface was cooked up by me without taking a look at the other Finance::Bank
382             modules. If you have any proposals for a change, they are welcome !
383              
384             =head1 WARNING
385              
386             This is code for online banking, and that means your money, and that means BE CAREFUL. You are encouraged, nay, expected, to audit the source of this module yourself to reassure yourself that I am not doing anything untoward with your banking data. This software is useful to me, but is provided under NO GUARANTEE, explicit or implied.
387              
388             =head1 WARNUNG
389              
390             Dieser Code beschaeftigt sich mit Online Banking, das heisst, hier geht es um Dein Geld und das bedeutet SEI VORSICHTIG ! Ich gehe
391             davon aus, dass Du den Quellcode persoenlich anschaust, um Dich zu vergewissern, dass ich nichts unrechtes mit Deinen Bankdaten
392             anfange. Diese Software finde ich persoenlich nuetzlich, aber ich stelle sie OHNE JEDE GARANTIE zur Verfuegung, weder eine
393             ausdrueckliche noch eine implizierte Garantie.
394              
395             =head1 METHODS
396              
397             =head2 new
398              
399             Creates a new object. It takes three named parameters :
400              
401             =over 4
402              
403             =item login => 'Petra.Pfiffig'
404              
405             This is your Postbank ID account name.
406              
407             =item password => '123456789'
408              
409             This is your PIN / password.
410              
411             =item status => sub {}
412              
413             This is an optional
414             parameter where you can specify a callback that will receive the messages the object
415             Finance::Bank::Postbank produces per session.
416              
417             =back
418              
419             =head2 $account->new_session
420              
421             Closes the current session and logs in to the website using
422             the credentials given at construction time.
423              
424             =head2 $account->close_session
425              
426             Closes the session and invalidates it on the server.
427              
428             =head2 $account->agent
429              
430             Returns the C<WWW::Mechanize> object. You can retrieve the
431             content of the current page from there.
432              
433             =head2 C<< $session->account_numbers >>
434              
435             Returns the account numbers. Only numeric account numbers
436             are returned - the credit card account numbers are not
437             returned.
438              
439             =head2 $account->select_function STRING
440              
441             Selects a function. The currently supported functions are
442              
443             accountstatement
444             quit
445              
446             =head2 $account->get_account_statement
447              
448             Navigates to the print version of the account statement. The content can currently
449             be retrieved from the agent, but this will most likely change, as the print version
450             of the account statement is not a navigable page. The result of the function
451             is either undef or a Finance::Bank::Postbank_de::Account object.
452              
453             C<past_days> - Number of days in the past to request the statement for
454             The default is 10.
455              
456             =head2 $account->unread_messages
457              
458             Returns the number of unread messages. There is no way
459             to retrieve the messages themselves yet.
460              
461             =head2 session_timed_out
462              
463             Returns true if our banking session timed out.
464              
465             =head2 maintenance
466              
467             Returns true if the banking interface is currently unavailable due to maintenance.
468              
469             =head1 AUTHOR
470              
471             Max Maischein, E<lt>corion@cpan.orgE<gt>
472              
473             =head1 SEE ALSO
474              
475             L<perl>, L<WWW::Mechanize>.
476              
477             =head1 REPOSITORY
478              
479             The public repository of this module is
480             L<https://github.com/Corion/Finance-Bank-Postbank_de>.
481              
482             =head1 SUPPORT
483              
484             The public support forum of this module is
485             L<https://perlmonks.org/>.
486              
487             =head1 BUG TRACKER
488              
489             Please report bugs in this module via the RT CPAN bug queue at
490             L<https://rt.cpan.org/Public/Dist/Display.html?Name=Finance-Bank-Postbank_de>
491             or via mail to L<finance-bank-postbank_de-Bugs@rt.cpan.org>.
492              
493             =head1 COPYRIGHT (c)
494              
495             Copyright 2003-2018 by Max Maischein C<corion@cpan.org>.
496              
497             =head1 LICENSE
498              
499             This module is released under the same terms as Perl itself.
500              
501             =cut