File Coverage

lib/Finance/Bank/JP/Mizuho.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


}. }. }. }. };
line stmt bran cond sub pod time code
1             =encoding utf8
2              
3             =head1 NAME
4              
5             Finance::Bank::JP::Mizuho
6              
7             =head1 SYNOPSIS
8              
9             my $mizuho = Finance::Bank::JP::Mizuho-new(
10             consumer_id => '123455678',
11             password => 'p45sW0rD',
12             questions => {
13             '母親の誕生日はいつですか(例:5月14日)' => '10月1日', # have to use 2byte digits, sucks
14             '最も年齢の近い兄弟姉妹の誕生日はいつですか(例:2月10日)' => '12月2日',
15             '応援しているスポーツチームの名前は何ですか' => '阪神タイガース',
16             },
17             );
18            
19             my $accounts = $mizuho->accounts;
20            
21             my $ofx = $mizuho->get_ofx(
22             $mizuho->accounts->[0],
23             $mizuho->CONTINUATION_FROM_LAST,
24             );
25              
26              
27             =head1 DESCRIPTION
28              
29             Perl interface to access your L account.
30              
31             =head1 CONSTANT
32              
33             =head2 CONTINUATION_FROM_LAST
34              
35             Value for L
36              
37             =head2 SAME_AS_LAST
38              
39             Value for L
40              
41             =head2 LAST_TWO_MONTHS
42              
43             Value for L
44              
45             =head1 FUNCTIONS
46              
47             =cut
48              
49             package Finance::Bank::JP::Mizuho;
50              
51 3     3   94436 use strict;
  3         8  
  3         110  
52 3     3   14 use warnings;
  3         6  
  3         74  
53              
54 3     3   16 use Carp;
  3         10  
  3         246  
55 3     3   4007 use DateTime;
  3         620134  
  3         124  
56 3     3   4648 use Encode;
  3         55512  
  3         476  
57 3     3   2029 use Finance::Bank::JP::Mizuho::Account;
  3         8  
  3         93  
58 3     3   1513 use Finance::OFX::Parse::Simple;
  0            
  0            
59             use HTTP::Cookies;
60             use LWP::UserAgent;
61              
62             our $VERSION = '0.02';
63              
64             use constant USER_AGENT => 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)';
65             use constant START_URL => 'http://www.mizuhobank.co.jp/direct/start.html';
66             use constant ENCODING => 'shift_jis';
67              
68             use constant CONTINUATION_FROM_LAST => 1;
69             use constant SAME_AS_LAST => 2;
70             use constant LAST_TWO_MONTHS => 3;
71              
72             =head2 new ( %config )
73              
74             Creates a new instance.
75              
76             C<%config> keys:
77              
78             =over 3
79              
80             =item
81             B
82              
83             Consumer id of Mizuho Direct ( お客さま番号 )
84              
85             =item
86             B
87              
88             Password for your consumer_id
89              
90             =item
91             B
92              
93             Hash reference paired with: Key as Question, Value as Answer
94              
95             =back
96              
97             =cut
98              
99             sub new {
100             my $class = shift;
101             my $self = bless { @_ }, $class;
102             $self;
103             }
104              
105             =head2 consumer_id
106              
107             =cut
108             sub consumer_id { shift->{consumer_id} }
109              
110             =head2 accounts
111              
112             returns array of L
113              
114             =cut
115              
116             sub accounts {
117             my $self = shift;
118             return $self->{accounts} if $self->{accounts};
119             return [] unless $self->login;
120             $self->parse_accounts( $self->get_content( $self->list_url ) );
121             }
122              
123             =head2 account_by_number ( $number )
124              
125             returns an instance of L
126              
127             =cut
128              
129             sub account_by_number {
130             my ( $self, $number ) = @_;
131             my @accounts = @{ $self->accounts };
132             return unless @accounts && $number;
133             foreach my $account ( @accounts ) {
134             return $account if $account->number eq $number;
135             }
136             }
137              
138             =head2 get_ofx ( $account_or_number , $term )
139              
140             C<$account_or_number>:
141             an instance of L OR bank account number
142              
143             C<$term> :
144              
145             =over 3
146              
147             =item L
148              
149             =item L
150              
151             =item L
152              
153             =back
154              
155             returns list of hash references, parsed by L
156              
157             =cut
158             sub get_ofx {
159             my $self = shift;
160              
161             Finance::OFX::Parse::Simple->parse_scalar($self->get_raw_ofx(@_));
162             }
163              
164             =head2 get_raw_ofx ( $account_or_number , $term )
165              
166             arguments are same as L.
167              
168             returns OFX content as scalar
169              
170             =cut
171             sub get_raw_ofx {
172             my ($self, $account, $term) = @_;
173            
174             $term ||= CONTINUATION_FROM_LAST;
175            
176             if( $term !~ /^(1|2|3)$/ ) {
177             carp( 'Invalid value to $term:'. $term );
178             return 0;
179             }
180            
181             my $val;
182             $account = $self->account_by_number( $account )
183             if( ref($account) ne 'Finance::Bank::JP::Mizuho::Account' );
184            
185             unless( $account ) {
186             carp( 'No account' );
187             return 0;
188             }
189              
190             $self->get_content( $self->ref_url );
191            
192             my $content = $self->get_content( $self->list_url );
193             my $emfpostkey = $self->emfpostkey( $content );
194             my $action = $self->form1_action( $content );
195            
196             unless( $emfpostkey && $action ) {
197             carp( 'Failed to parse page' );
198             return 0;
199             }
200            
201             my $res = $self->ua->post(
202             $action,
203             Referer => $self->list_url,
204             Content => [
205             Token => '',
206             REDISP => 'OFF',
207             NLS => 'JP',
208             EMFPOSTKEY => $emfpostkey,
209             SelectRadio => $account->radio_value,
210             DownhaniBox => $term,
211             Next => 'Yippee!',
212             ],
213             );
214            
215             my $dest = $res->header('location');
216            
217             unless( $dest ) {
218             carp( 'Query fail' );
219             return 0;
220             }
221            
222             $content = $self->get_content( $dest );
223             $emfpostkey = $self->emfpostkey( $content );
224             $action = $self->form1_action( $content );
225            
226             $res = $self->ua->post(
227             $action,
228             Referer => $action,
229             Content => [
230             Token => '',
231             NLS => 'JP',
232             EMFPOSTKEY => $emfpostkey,
233             ],
234             );
235            
236             $dest = $res->header('location');
237             unless( $dest ) {
238             carp( 'Query fail' );
239             return 0;
240             }
241            
242             $content = $self->get_content( $dest );
243             $emfpostkey = $self->emfpostkey( $content );
244             $action = $self->form1_action( $content );
245            
246             my $ofx = $self->ua->get( $self->ofx_url )->content;
247            
248             $res = $self->ua->post(
249             $action,
250             Referer => $action,
251             Content => [
252             Token => '',
253             NLS => 'JP',
254             EMFPOSTKEY => $emfpostkey,
255             ],
256             );
257            
258             $ofx
259             }
260              
261             =head2 host
262              
263             returns random host, provided by Mizuho Direct web service.
264              
265             =cut
266             sub host {
267             my $self = shift;
268             if(@_) {
269             my $host = shift;
270             $self->ua->default_headers->header(
271             Origin => "https://$host",
272             Host => $host,
273             );
274             $self->{host} = $host;
275             }
276             return $self->{host} if $self->{host};
277             $self->{host} || 'web.ib.mizuhobank.co.jp'
278             }
279              
280              
281             =head2 logged_in
282              
283             returns this instance has logged in
284              
285             =cut
286             sub logged_in {
287             my $self = shift;
288             $self->{logged_in} = shift if @_;
289             $self->{logged_in};
290             }
291              
292              
293             =head2 login
294              
295             returns logged in successfully.
296              
297             calling this method is not neccesary.
298              
299             =cut
300              
301             sub login {
302             my $self = shift;
303             return 1 if $self->logged_in;
304             my $url = $self->login_url2;
305             ($url=~m{xtr=Emf00005}) ?
306             $self->_login($url) :
307             $self->_question($url);
308             }
309              
310             =head2 logout
311              
312             if you leave the process without calling this method,
313             the account will be locked for about 10 minutes,
314             and you will not able to access the web service.
315              
316             =cut
317              
318             sub logout {
319             my $self = shift;
320             return unless $self->logged_in;
321             my $res = $self->ua->get($self->logout_url);
322             $self->logged_in(0);
323             }
324              
325             =head2 password
326              
327             =cut
328             sub password { shift->{password} }
329              
330             =head2 questions
331              
332             =cut
333             sub questions { shift->{questions} }
334              
335             =head2 ua
336              
337             =cut
338              
339             sub ua {
340             shift->{ua} ||= LWP::UserAgent->new(
341             agent => USER_AGENT,
342             cookie_jar => HTTP::Cookies->new,
343             max_redirect => 0,
344             requests_redirectable => [],
345             )
346             }
347              
348             ## private __________________________________________________________________________________________
349              
350             sub login_url1 { 'https://'. shift->host .'/servlet/mib?xtr=Emf00000' }
351             sub logout_url { 'https://'. shift->host . ':443/servlet/mib?xtr=EmfLogOff&NLS=JP' }
352             sub ref_url { 'https://'. shift->host . '/servlet/mib?xtr=Emf04000&NLS=JP' }
353             sub list_url { 'https://'. shift->host . '/servlet/mib?xtr=Emf04610&NLS=JP' }
354             sub ofx_url { 'https://'. shift->host . ':443/servlet/mib?xtr=Emf04625' }
355              
356             sub login_url2 {
357             my $self = shift;
358             my $action = $self->form1_action($self->get_content($self->login_url1));
359             my $res = $self->ua->post( $action, [
360             pm_fp => '',
361             KeiyakuNo => $self->consumer_id,
362             Next => 'Yippee!',
363             ]);
364             my $url = $res->header('location') || '';
365             $self->host($1) if $url =~ m%^https://([^/\:]+).*%;
366             $url;
367             }
368              
369             sub _question {
370             my ($self,$url) = @_;
371             my $content = $self->get_content($url);
372             my $action = $self->form1_action($content);
373             my ( $question, $answer );
374             unless( $question = $self->parse_question($content) ) {
375             carp('Failed to parse question screen');
376             return 0;
377             }
378             unless( $answer = $self->questions->{$question} ) {
379             carp("No answer for '$question'");
380             return 0;
381             }
382             my $res = $self->ua->post( $action, [
383             rskAns => encode(ENCODING, decode('utf8', $answer) ),
384             Next => 'Yippee!',
385             NLS => 'JP',
386             Token => '',
387             jsAware => 'on',
388             frmScrnID => 'Emf00000',
389             ]);
390             my $dest = $res->header('location');
391             unless($dest) {
392             carp('Login failure');
393             return 0;
394             }
395             $dest eq $url ?
396             $self->_question($url) :
397             $self->_login($dest);
398             }
399              
400             sub _login {
401             my ($self,$url) = @_;
402             my $content = $self->get_content($url);
403             my $action = $self->form1_action($content);
404             my $res = $self->ua->post( $action, [
405             NLS => 'JP',
406             jsAware => 'on',
407             pmimg => '0',
408             Anshu1No => $self->password,
409             login => 'Yippee!',
410             ]);
411             my $dest = $res->header('location');
412             return 0 unless $dest;
413             $self->logged_in(1);
414             1
415             }
416              
417             sub parse_question {
418             my ($self,$content) = @_;
419             return $1 if( ( $content || '' ) =~ /.*
.+[^\n\r]*[\n\r].*]*>([^<]+)<.*/i );
420             ''
421             }
422              
423             sub parse_accounts {
424             my ($self,$content) = @_;
425             $content =~ s/[\s"\r\n\t]//g;
426             my $re =
427             q{]*>]*>
428             q{]*>]*> ([^<]+)
429             q{]*>]*> ([^<]+)
430             q{]*>]*>(\d+)
431             q{]*>]*>([^<]+)
432              
433             my @tr = split /TR>
434             my @accounts = ();
435             my $tz = 'Asia/Tokyo';
436             foreach my $t (@tr) {
437             if($t =~ /$re/i) {
438             my $obj = {
439             radio_value => $1,
440             branch => $2,
441             type => $3,
442             number => $4,
443             };
444             my $d = $5;
445             my ($start, $end);
446             if( $d =~ /(\d{4})\.(\d{2})\.(\d{2})[^\d]+(\d{4})\.(\d{2})\.(\d{2})/ ) {
447             $start = DateTime->new(
448             year => $1,
449             month => $2,
450             day => $3,
451             time_zone => $tz,
452             );
453             $end = DateTime->new(
454             year => $4,
455             month => $5,
456             day => $6,
457             time_zone => $tz,
458             );
459             } elsif( $d =~ /(\d{4})\.(\d{2})\.(\d{2})/ ) {
460             $start = DateTime->new(
461             year => $1,
462             month => $2,
463             day => $3,
464             time_zone => $tz,
465             );
466             }
467             $end ||= $start;
468             $obj->{last_downloaded_from} = $start if $start;
469             $obj->{last_downloaded_to} = $end if $end;
470             push @accounts, Finance::Bank::JP::Mizuho::Account->new(%$obj);
471             }
472             }
473             $self->{accounts} = [@accounts];
474             }
475              
476             sub form1_action {
477             my ($self,$content) = @_;
478             return $1 if $content =~ /.*action="([^"]+)"[^\n\r]+name="FORM1".*/ig;
479             return $1 if $content =~ /.*name="FORM1"[^\n\r]+action="([^"]+)".*/ig;
480             ''
481             }
482              
483             sub emfpostkey {
484             my ($self,$content) = @_;
485             return $1 if ( $content =~ //i );
486             ''
487             }
488              
489             sub get_content {
490             my ($self,$url) = @_;
491             my $res = $self->ua->get($url);
492             $self->ua->default_headers->header( Referer => $url );
493             encode('utf8', decode(ENCODING,$res->content) );
494             }
495              
496              
497              
498              
499             1
500              
501             __END__