71513: Card tokenization [v3 test tweak]
[freeside.git] / FS / t / suite / 13-tokenization.t
1 #!/usr/bin/perl
2
3 use strict;
4 use FS::Test;
5 use Test::More;
6 use FS::Conf;
7 use FS::cust_main;
8 use Business::CreditCard qw(generate_last_digit);
9 use DateTime;
10 if ( stat('/usr/local/etc/freeside/cardfortresstest.txt') ) {
11   plan tests => 21;
12 } else {
13   plan skip_all => 'CardFortress test encryption key is not installed.';
14 }
15
16 ### can only run on test database (company name "Freeside Test")
17 ### will run upgrade, which uses lots of prints & warns beyond regular test output
18
19 my $fs = FS::Test->new( user => 'admin' );
20 my $conf = FS::Conf->new;
21 my $err;
22 my $bopconf;
23
24 like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT('');
25
26 # upgrade just schema, or v3 test db cannot create refunds
27 $err = system('freeside-upgrade','-s','admin');
28 ok( !$err, 'schema upgrade' ) or BAIL_OUT('Error string: '.$!);
29
30 # some pre-upgrade cleanup, upgrade will fail if these are still configured
31 foreach my $cust_main ( $fs->qsearch('cust_main') ) {
32   my @count = $fs->qsearch('agent_payment_gateway', { agentnum => $cust_main->agentnum } );
33   if (@count > 0) {
34     note("DELETING CARDTYPE GATEWAYS");
35     foreach my $apg (@count) {
36       $err = $apg->delete;
37       last if $err;
38     }
39     @count = $fs->qsearch('agent_payment_gateway', { agentnum => $cust_main->agentnum } );
40     if (@count > 1) {
41       $err = "Still found ".@count." gateways for custnum ".$cust_main->custnum;
42       last;
43     }
44   }
45 }
46 ok( !$err, "remove obsolete payment gateways" ) or BAIL_OUT($err);
47
48 $bopconf = 
49 'IPPay
50 TESTTERMINAL';
51 $conf->set('business-onlinepayment' => $bopconf);
52 is( join("\n",$conf->config('business-onlinepayment')), $bopconf, "setting first default gateway" ) or BAIL_OUT('');
53
54 # generate a few void/refund records for upgrading
55 my $counter = 20;
56 foreach my $cust_pay ( $fs->qsearch('cust_pay',{ payby => 'CARD' }) ) {
57   if ($counter % 2) {
58     $err = $cust_pay->void('Testing');
59     $err = "Voiding: $err" if $err;
60   } else {
61     if ($fs->qsearch('cust_refund',{ source_paynum => $cust_pay->paynum })) {
62       note('refund skipping cust_pay '.$cust_pay->paynum.', already refunded');
63       next;
64     }
65     # from realtime_refund_bop, just the important bits    
66     while ( $cust_pay->unapplied < $cust_pay->paid ) {
67       my @cust_bill_pay = $cust_pay->cust_bill_pay;
68       last unless @cust_bill_pay;
69       my $cust_bill_pay = pop @cust_bill_pay;
70       $err = $cust_bill_pay->delete;
71       $err = "Refund unapply: $err" if $err;
72       last if $err;
73     }
74     last if $err;
75     my $cust_refund = new FS::cust_refund ( {
76       'custnum'  => $cust_pay->cust_main->custnum,
77       'paynum'   => $cust_pay->paynum,
78       'source_paynum' => $cust_pay->paynum,
79       'refund'   => $cust_pay->paid,
80       '_date'    => '',
81       'payby'    => $cust_pay->payby,
82       'payinfo'  => $cust_pay->payinfo,
83       'reason'     => 'Testing',
84       'gatewaynum'    => $cust_pay->gatewaynum,
85       'processor'     => $cust_pay->payment_gateway ? $cust_pay->payment_gateway->processor : '',
86       'auth'          => $cust_pay->auth,
87       'order_number'  => $cust_pay->order_number,
88     } );
89     $err = $cust_refund->insert( reason_type => 'Refund' );
90     $err = "Refunding ".$cust_pay->paynum.": $err" if $err;
91   }
92   last if $err;
93   $counter -= 1;
94   last unless $counter > 0;
95 }
96 $err ||= 'not enough records' if $counter;
97 ok( !$err, "create some refunds and voids" ) or BAIL_OUT($err);
98
99 # also, just to test behavior in this case, create a record for an aborted
100 # verification payment. this will have no customer number.
101
102 my $pending_failed = FS::cust_pay_pending->new({
103   'custnum_pending' => 1,
104   'paid'    => '1.00',
105   '_date'   => time - 86400,
106   random_card(),
107   'status'  => 'failed',
108   'statustext' => 'Tokenization upgrade test',
109 });
110 $err = $pending_failed->insert;
111 ok( !$err, "create a failed payment attempt" ) or BAIL_OUT($err);
112
113 # find two stored credit cards, and one stored checking account (to overwrite with CARD)
114 my @cust = (
115   $fs->qsearch({ table=>'cust_main', hashref=>{payby=>'CARD'}, extra_sql=>' LIMIT 2' }),
116   $fs->qsearch({ table=>'cust_main', hashref=>{payby=>'CHEK'}, extra_sql=>' LIMIT 1' }),
117 );
118 my @payment;
119
120 ok( $cust[0]->payby eq 'CARD' && !$cust[0]->tokenized,
121   "first customer has a non-tokenized card"
122   ) or BAIL_OUT();
123
124 $err = $cust[0]->realtime_bop({method => 'CC', amount => '2.00'});
125 ok( !$err, "create a payment through IPPay" )
126   or BAIL_OUT($err);
127 $payment[0] = $fs->qsearchs('cust_pay', { custnum => $cust[0]->custnum,
128                                      paid => '2.00' })
129   or BAIL_OUT("can't find payment record");
130
131 $err = system('freeside-upgrade','admin');
132 ok( !$err, 'initial upgrade' ) or BAIL_OUT('Error string: '.$!);
133
134 # switch to CardFortress
135 $bopconf =
136 'CardFortress
137 cardfortresstest
138 (TEST54)
139 Normal Authorization
140 gateway
141 IPPay
142 gateway_login
143 TESTTERMINAL
144 gateway_password
145
146 private_key
147 /usr/local/etc/freeside/cardfortresstest.txt';
148 $conf->set('business-onlinepayment' => $bopconf);
149 is( join("\n",$conf->config('business-onlinepayment')), $bopconf, "setting tokenizable default gateway" ) or BAIL_OUT('');
150
151 foreach my $pg ($fs->qsearch('payment_gateway')) {
152   unless ($pg->gateway_module eq 'CardFortress') {
153     note('UPGRADING NON-CF PAYMENT GATEWAY');
154     my %pgopts = (
155       gateway          => $pg->gateway_module,
156       gateway_login    => $pg->gateway_username,
157       gateway_password => $pg->gateway_password,
158       private_key      => '/usr/local/etc/freeside/cardfortresstest.txt',
159     );
160     $pg->gateway_module('CardFortress');
161     $pg->gateway_username('cardfortresstest');
162     $pg->gateway_password('(TEST54)');
163     $err = $pg->replace(\%pgopts);
164     last if $err;
165   }
166 }
167 ok( !$err, "remove non-CF payment gateways" ) or BAIL_OUT($err);
168
169 # create a payment using a non-tokenized card. this should immediately
170 # trigger tokenization.
171 ok( $cust[1]->payby eq 'CARD' && ! $cust[1]->tokenized,
172   "second customer has a non-tokenized card"
173   ) or BAIL_OUT();
174
175 $err = $cust[1]->realtime_bop({method => 'CC', amount => '3.00'});
176 ok( !$err, "tokenize a card when it's first used for payment" )
177   or BAIL_OUT($err);
178 $payment[1] = $fs->qsearchs('cust_pay', { custnum => $cust[1]->custnum,
179                                      paid => '3.00' })
180   or BAIL_OUT("can't find payment record");
181 ok( $payment[1]->tokenized, "payment is tokenized" );
182 $cust[1] = $cust[1]->replace_old;
183 ok( $cust[1]->tokenized, "card is now tokenized" );
184
185
186 # invoke the part of freeside-upgrade that tokenizes
187 FS::cust_main->queueable_upgrade();
188 #$err = system('freeside-upgrade','admin');
189 #ok( !$err, 'tokenizable upgrade' ) or BAIL_OUT('Error string: '.$!);
190
191 $cust[0] = $cust[0]->replace_old;
192 ok( $cust[0]->tokenized, "old card was tokenized during upgrade" );
193 $payment[0] = $payment[0]->replace_old;
194 ok( $payment[0]->tokenized, "old payment was tokenized during upgrade" );
195 my $old_pending = $fs->qsearchs('cust_pay_pending',{ paynum => $payment[0]->paynum });
196 ok( $old_pending->tokenized, "old cust_pay_pending was tokenized during upgrade" );
197
198 $pending_failed = $pending_failed->replace_old;
199 ok( $pending_failed->tokenized, "cust_pay_pending with no customer was tokenized" );
200
201 # add a new payment card to one customer
202 my %newcard = random_card();
203 $cust[2]->$_($newcard{$_}) foreach keys %newcard;
204 $err = $cust[2]->replace;
205 ok( !$err, "new card was saved" ) or BAIL_OUT($err);
206 ok($cust[2]->tokenized, "new card is tokenized" );
207
208 sub random_card {
209   my $payinfo = '4111' . join('', map { int(rand(10)) } 1 .. 11);
210   $payinfo .= generate_last_digit($payinfo);
211   my $paydate = DateTime->now
212                 ->add('years' => 1)
213                 ->truncate(to => 'month')
214                 ->strftime('%F');
215   return ( 'payby'    => 'CARD',
216            'payinfo'  => $payinfo,
217            'paydate'  => $paydate,
218            'payname'  => 'Tokenize Me',
219   );
220 }
221
222 1;
223
224