1 package FS::cust_payby;
4 use base qw( FS::payinfo_Mixin FS::Record );
6 use FS::Record qw( qsearchs ); #qsearch;
10 use vars qw( $conf $ignore_expired_card $ignore_banned_card );
12 $ignore_expired_card = 0;
13 $ignore_banned_card = 0;
15 install_callback FS::UID sub {
17 #yes, need it for stuff below (prolly should be cached)
22 FS::cust_payby - Object methods for cust_payby records
28 $record = new FS::cust_payby \%hash;
29 $record = new FS::cust_payby { 'column' => 'value' };
31 $error = $record->insert;
33 $error = $new_record->replace($old_record);
35 $error = $record->delete;
37 $error = $record->check;
41 An FS::cust_payby object represents customer stored payment information.
42 FS::cust_payby inherits from FS::Record. The following fields are currently
116 Creates a new record. To add the record to the database, see L<"insert">.
118 Note that this stores the hash reference, not a distinct copy of the hash it
119 points to. You can ask the object for a copy with the I<hash> method.
123 # the new method can be inherited from FS::Record, if a table method is defined
125 sub table { 'cust_payby'; }
129 Adds this record to the database. If there is an error, returns the error,
130 otherwise returns false.
134 # the insert method can be inherited from FS::Record
138 Delete this record from the database.
142 # the delete method can be inherited from FS::Record
144 =item replace OLD_RECORD
146 Replaces the OLD_RECORD with this one in the database. If there is an error,
147 returns the error, otherwise returns false.
151 # the replace method can be inherited from FS::Record
155 Checks all fields to make sure this is a valid record. If there is
156 an error, returns the error, otherwise returns false. Called by the insert
165 $self->ut_numbern('custpaybynum')
166 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
167 || $self->ut_number('weight')
168 || $self->ut_('payby')
169 || $self->ut_textn('payinfo')
170 || $self->ut_textn('paycvv')
171 || $self->ut_textn('paymask')
172 || $self->ut_textn('paydate')
173 || $self->ut_numbern('paystart_month')
174 || $self->ut_numbern('paystart_year')
175 || $self->ut_textn('payissue')
176 || $self->ut_textn('payname')
177 || $self->ut_textn('paystate')
178 || $self->ut_textn('paytype')
179 || $self->ut_textn('payip')
181 return $error if $error;
187 #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
188 # or return "Illegal payby: ". $self->payby;
190 FS::payby->can_payby($self->table, $self->payby)
191 or return "Illegal payby: ". $self->payby;
193 $error = $self->ut_numbern('paystart_month')
194 || $self->ut_numbern('paystart_year')
195 || $self->ut_numbern('payissue')
196 || $self->ut_textn('paytype')
198 return $error if $error;
200 if ( $self->payip eq '' ) {
203 $error = $self->ut_ip('payip');
204 return $error if $error;
207 # If it is encrypted and the private key is not availaible then we can't
208 # check the credit card.
209 my $check_payinfo = ! $self->is_encrypted($self->payinfo);
211 # Need some kind of global flag to accept invalid cards, for testing
213 #XXX if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
214 if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
216 my $payinfo = $self->payinfo;
218 $payinfo =~ /^(\d{13,16}|\d{8,9})$/
219 or return gettext('invalid_card'); # . ": ". $self->payinfo;
221 $self->payinfo($payinfo);
223 or return gettext('invalid_card'); # . ": ". $self->payinfo;
225 return gettext('unknown_card_type')
226 if $self->payinfo !~ /^99\d{14}$/ #token
227 && cardtype($self->payinfo) eq "Unknown";
229 unless ( $ignore_banned_card ) {
230 my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
232 if ( $ban->bantype eq 'warn' ) {
233 #or others depending on value of $ban->reason ?
234 return '_duplicate_card'.
235 ': disabled from'. time2str('%a %h %o at %r', $ban->_date).
236 ' until '. time2str('%a %h %o at %r', $ban->_end_date).
237 ' (ban# '. $ban->bannum. ')'
238 unless $self->override_ban_warn;
240 return 'Banned credit card: banned on '.
241 time2str('%a %h %o at %r', $ban->_date).
242 ' by '. $ban->otaker.
243 ' (ban# '. $ban->bannum. ')';
248 if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
249 if ( cardtype($self->payinfo) eq 'American Express card' ) {
250 $self->paycvv =~ /^(\d{4})$/
251 or return "CVV2 (CID) for American Express cards is four digits.";
254 $self->paycvv =~ /^(\d{3})$/
255 or return "CVV2 (CVC2/CID) is three digits.";
262 my $cardtype = cardtype($payinfo);
263 if ( $cardtype =~ /^(Switch|Solo)$/i ) {
265 return "Start date or issue number is required for $cardtype cards"
266 unless $self->paystart_month && $self->paystart_year or $self->payissue;
268 return "Start month must be between 1 and 12"
269 if $self->paystart_month
270 and $self->paystart_month < 1 || $self->paystart_month > 12;
272 return "Start year must be 1990 or later"
273 if $self->paystart_year
274 and $self->paystart_year < 1990;
276 return "Issue number must be beween 1 and 99"
278 and $self->payissue < 1 || $self->payissue > 99;
281 $self->paystart_month('');
282 $self->paystart_year('');
286 } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
288 my $payinfo = $self->payinfo;
289 $payinfo =~ s/[^\d\@\.]//g;
290 if ( $conf->config('echeck-country') eq 'CA' ) {
291 $payinfo =~ /^(\d+)\@(\d{5})\.(\d{3})$/
292 or return 'invalid echeck account@branch.bank';
293 $payinfo = "$1\@$2.$3";
294 } elsif ( $conf->config('echeck-country') eq 'US' ) {
295 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
298 $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@routing';
301 $self->payinfo($payinfo);
304 unless ( $ignore_banned_card ) {
305 my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
307 if ( $ban->bantype eq 'warn' ) {
308 #or others depending on value of $ban->reason ?
309 return '_duplicate_ach' unless $self->override_ban_warn;
311 return 'Banned ACH account: banned on '.
312 time2str('%a %h %o at %r', $ban->_date).
313 ' by '. $ban->otaker.
314 ' (ban# '. $ban->bannum. ')';
319 } elsif ( $self->payby eq 'LECB' ) {
321 my $payinfo = $self->payinfo;
323 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
325 $self->payinfo($payinfo);
328 } elsif ( $self->payby eq 'BILL' ) {
330 $error = $self->ut_textn('payinfo');
331 return "Illegal P.O. number: ". $self->payinfo if $error;
334 } elsif ( $self->payby eq 'COMP' ) {
336 my $curuser = $FS::CurrentUser::CurrentUser;
337 if ( ! $self->custnum
338 && ! $curuser->access_right('Complimentary customer')
341 return "You are not permitted to create complimentary accounts."
344 $error = $self->ut_textn('payinfo');
345 return "Illegal comp account issuer: ". $self->payinfo if $error;
348 } elsif ( $self->payby eq 'PREPAY' ) {
350 my $payinfo = $self->payinfo;
351 $payinfo =~ s/\W//g; #anything else would just confuse things
352 $self->payinfo($payinfo);
353 $error = $self->ut_alpha('payinfo');
354 return "Illegal prepayment identifier: ". $self->payinfo if $error;
355 return "Unknown prepayment identifier"
356 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
361 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
362 return "Expiration date required"
363 # shouldn't payinfo_check do this?
364 unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
368 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
369 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
370 } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
371 ( $m, $y ) = ( $2, "19$1" );
372 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
373 ( $m, $y ) = ( $3, "20$2" );
375 return "Illegal expiration date: ". $self->paydate;
377 $m = sprintf('%02d',$m);
378 $self->paydate("$y-$m-01");
379 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
380 return gettext('expired_card')
383 !$ignore_expired_card
384 && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
387 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
388 ( ! $conf->exists('require_cardname')
389 || $self->payby !~ /^(CARD|DCRD)$/ )
391 $self->payname( $self->first. " ". $self->getfield('last') );
393 $self->payname =~ /^([\w \,\.\-\'\&]+)$/
394 or return gettext('illegal_name'). " payname: ". $self->payname;
409 L<FS::Record>, schema.html from the base documentation.