Use any card on file when making a payment, RT#23741
[freeside.git] / FS / FS / cust_payby.pm
1 package FS::cust_payby;
2 use base qw( FS::payinfo_Mixin FS::cust_main_Mixin FS::Record );
3
4 use strict;
5 use Scalar::Util qw( blessed );
6 use Digest::SHA qw( sha512_base64 );
7 use Business::CreditCard qw( validate cardtype );
8 use FS::UID qw( dbh );
9 use FS::Msgcat qw( gettext );
10 use FS::Misc qw( card_types );
11 use FS::Record; #qw( qsearch qsearchs );
12 use FS::payby;
13 use FS::cust_main;
14 use FS::banned_pay;
15
16 our @encrypted_fields = ('payinfo', 'paycvv');
17 sub nohistory_fields { ('payinfo', 'paycvv'); }
18
19 our $ignore_expired_card = 0;
20 our $ignore_banned_card = 0;
21 our $ignore_invalid_card = 0;
22
23 our $conf;
24 install_callback FS::UID sub { 
25   $conf = new FS::Conf;
26   #yes, need it for stuff below (prolly should be cached)
27   $ignore_invalid_card = $conf->exists('allow_invalid_cards');
28 };
29
30 =head1 NAME
31
32 FS::cust_payby - Object methods for cust_payby records
33
34 =head1 SYNOPSIS
35
36   use FS::cust_payby;
37
38   $record = new FS::cust_payby \%hash;
39   $record = new FS::cust_payby { 'column' => 'value' };
40
41   $error = $record->insert;
42
43   $error = $new_record->replace($old_record);
44
45   $error = $record->delete;
46
47   $error = $record->check;
48
49 =head1 DESCRIPTION
50
51 An FS::cust_payby object represents customer stored payment information.
52 FS::cust_payby inherits from FS::Record.  The following fields are currently
53 supported:
54
55 =over 4
56
57 =item custpaybynum
58
59 primary key
60
61 =item custnum
62
63 custnum
64
65 =item weight
66
67 weight
68
69 =item payby
70
71 payby
72
73 =item payinfo
74
75 payinfo
76
77 =item paycvv
78
79 paycvv
80
81 =item paymask
82
83 paymask
84
85 =item paydate
86
87 paydate
88
89 =item paystart_month
90
91 paystart_month
92
93 =item paystart_year
94
95 paystart_year
96
97 =item payissue
98
99 payissue
100
101 =item payname
102
103 payname
104
105 =item paystate
106
107 paystate
108
109 =item paytype
110
111 paytype
112
113 =item payip
114
115 payip
116
117
118 =back
119
120 =head1 METHODS
121
122 =over 4
123
124 =item new HASHREF
125
126 Creates a new record.  To add the record to the database, see L<"insert">.
127
128 Note that this stores the hash reference, not a distinct copy of the hash it
129 points to.  You can ask the object for a copy with the I<hash> method.
130
131 =cut
132
133 # the new method can be inherited from FS::Record, if a table method is defined
134
135 sub table { 'cust_payby'; }
136
137 =item insert
138
139 Adds this record to the database.  If there is an error, returns the error,
140 otherwise returns false.
141
142 =cut
143
144 sub insert {
145   my $self = shift;
146
147   local $SIG{HUP} = 'IGNORE';
148   local $SIG{INT} = 'IGNORE';
149   local $SIG{QUIT} = 'IGNORE';
150   local $SIG{TERM} = 'IGNORE';
151   local $SIG{TSTP} = 'IGNORE';
152   local $SIG{PIPE} = 'IGNORE';
153
154   my $oldAutoCommit = $FS::UID::AutoCommit;
155   local $FS::UID::AutoCommit = 0;
156   my $dbh = dbh;
157
158   my $error =  $self->check_payinfo_cardtype
159             || $self->SUPER::insert;
160   if ( $error ) {
161     $dbh->rollback if $oldAutoCommit;
162     return $error;
163   }
164
165   if ( $self->payby =~ /^(CARD|CHEK)$/ ) {
166     # new auto card/check info, want to retry realtime_ invoice events
167     #  (new customer?  that's okay, they won't have any)
168     my $error = $self->cust_main->retry_realtime;
169     if ( $error ) {
170       $dbh->rollback if $oldAutoCommit;
171       return $error;
172     }
173   }
174
175   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
176   '';
177
178 }
179
180 =item delete
181
182 Delete this record from the database.
183
184 =item replace OLD_RECORD
185
186 Replaces the OLD_RECORD with this one in the database.  If there is an error,
187 returns the error, otherwise returns false.
188
189 =cut
190
191 sub replace {
192   my $self = shift;
193
194   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
195               ? shift
196               : $self->replace_old;
197
198   if ( length($old->paycvv) && $self->paycvv =~ /^\s*[\*x]*\s*$/ ) {
199     $self->paycvv($old->paycvv);
200   }
201
202   if ( $self->payby =~ /^(CARD|DCRD)$/
203        && (    $self->payinfo =~ /xx/
204             || $self->payinfo =~ /^\s*N\/A\s+\(tokenized\)\s*$/
205           )
206      )
207   {
208
209     $self->payinfo($old->payinfo);
210
211   } elsif ( $self->payby =~ /^(CHEK|DCHK)$/ && $self->payinfo =~ /xx/ ) {
212     #fix for #3085 "edit of customer's routing code only surprisingly causes
213     #nothing to happen...
214     # this probably won't do the right thing when we don't have the
215     # public key (can't actually get the real $old->payinfo)
216     my($new_account, $new_aba) = split('@', $self->payinfo);
217     my($old_account, $old_aba) = split('@', $old->payinfo);
218     $new_account = $old_account if $new_account =~ /xx/;
219     $new_aba     = $old_aba     if $new_aba     =~ /xx/;
220     $self->payinfo($new_account.'@'.$new_aba);
221   }
222
223   local($ignore_expired_card) = 1
224     if $old->payby  =~ /^(CARD|DCRD)$/
225     && $self->payby =~ /^(CARD|DCRD)$/
226     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
227
228   local($ignore_banned_card) = 1
229     if (    $old->payby  =~ /^(CARD|DCRD)$/ && $self->payby =~ /^(CARD|DCRD)$/
230          || $old->payby  =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ )
231     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
232
233   if (    $self->payby =~ /^(CARD|DCRD)$/
234        && $old->payinfo ne $self->payinfo
235        && $old->paymask ne $self->paymask )
236   {
237     my $error = $self->check_payinfo_cardtype;
238     return $error if $error;
239   }
240
241   local $SIG{HUP} = 'IGNORE';
242   local $SIG{INT} = 'IGNORE';
243   local $SIG{QUIT} = 'IGNORE';
244   local $SIG{TERM} = 'IGNORE';
245   local $SIG{TSTP} = 'IGNORE';
246   local $SIG{PIPE} = 'IGNORE';
247
248   my $oldAutoCommit = $FS::UID::AutoCommit;
249   local $FS::UID::AutoCommit = 0;
250   my $dbh = dbh;
251
252   my $error = $self->SUPER::replace($old);
253   if ( $error ) {
254     $dbh->rollback if $oldAutoCommit;
255     return $error;
256   }
257
258   if ( $self->payby =~ /^(CARD|CHEK)$/
259        && ( ( $self->get('payinfo') ne $old->get('payinfo')
260               && $self->get('payinfo') !~ /^99\d{14}$/ 
261             )
262             || grep { $self->get($_) ne $old->get($_) } qw(paydate payname)
263           )
264      )
265   {
266
267     # card/check/lec info has changed, want to retry realtime_ invoice events
268     my $error = $self->cust_main->retry_realtime;
269     if ( $error ) {
270       $dbh->rollback if $oldAutoCommit;
271       return $error;
272     }
273   }
274
275   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
276   '';
277
278 }
279
280 =item check
281
282 Checks all fields to make sure this is a valid record.  If there is
283 an error, returns the error, otherwise returns false.  Called by the insert
284 and replace methods.
285
286 =cut
287
288 sub check {
289   my $self = shift;
290
291   my $error = 
292     $self->ut_numbern('custpaybynum')
293     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
294     || $self->ut_numbern('weight')
295     #encrypted #|| $self->ut_textn('payinfo')
296     #encrypted #|| $self->ut_textn('paycvv')
297 #    || $self->ut_textn('paymask') #XXX something
298     #later #|| $self->ut_textn('paydate')
299     || $self->ut_numbern('paystart_month')
300     || $self->ut_numbern('paystart_year')
301     || $self->ut_numbern('payissue')
302 #    || $self->ut_textn('payname') #XXX something
303     || $self->ut_alphan('paystate')
304     || $self->ut_textn('paytype')
305     || $self->ut_ipn('payip')
306   ;
307   return $error if $error;
308
309   ### from cust_main
310
311   FS::payby->can_payby($self->table, $self->payby)
312     or return "Illegal payby: ". $self->payby;
313
314   # If it is encrypted and the private key is not availaible then we can't
315   # check the credit card.
316   my $check_payinfo = ! $self->is_encrypted($self->payinfo);
317
318   # Need some kind of global flag to accept invalid cards, for testing
319   # on scrubbed data.
320   #XXX if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
321   if ( !$ignore_invalid_card && 
322     $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
323
324     my $payinfo = $self->payinfo;
325     $payinfo =~ s/\D//g;
326     $payinfo =~ /^(\d{13,16}|\d{8,9})$/
327       or return gettext('invalid_card'); #. ": ". $self->payinfo;
328     $payinfo = $1;
329     $self->payinfo($payinfo);
330     validate($payinfo)
331       or return gettext('invalid_card'); # . ": ". $self->payinfo;
332
333     return gettext('unknown_card_type')
334       if $self->payinfo !~ /^99\d{14}$/ #token
335       && cardtype($self->payinfo) eq "Unknown";
336
337     unless ( $ignore_banned_card ) {
338       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
339       if ( $ban ) {
340         if ( $ban->bantype eq 'warn' ) {
341           #or others depending on value of $ban->reason ?
342           return '_duplicate_card'.
343                  ': disabled from'. time2str('%a %h %o at %r', $ban->_date).
344                  ' until '.         time2str('%a %h %o at %r', $ban->_end_date).
345                  ' (ban# '. $ban->bannum. ')'
346             unless $self->override_ban_warn;
347         } else {
348           return 'Banned credit card: banned on '.
349                  time2str('%a %h %o at %r', $ban->_date).
350                  ' by '. $ban->otaker.
351                  ' (ban# '. $ban->bannum. ')';
352         }
353       }
354     }
355
356     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
357       if ( cardtype($self->payinfo) eq 'American Express card' ) {
358         $self->paycvv =~ /^(\d{4})$/
359           or return "CVV2 (CID) for American Express cards is four digits.";
360         $self->paycvv($1);
361       } else {
362         $self->paycvv =~ /^(\d{3})$/
363           or return "CVV2 (CVC2/CID) is three digits.";
364         $self->paycvv($1);
365       }
366     } else {
367       $self->paycvv('');
368     }
369
370     my $cardtype = cardtype($payinfo);
371     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
372
373       return "Start date or issue number is required for $cardtype cards"
374         unless $self->paystart_month && $self->paystart_year or $self->payissue;
375
376       return "Start month must be between 1 and 12"
377         if $self->paystart_month
378            and $self->paystart_month < 1 || $self->paystart_month > 12;
379
380       return "Start year must be 1990 or later"
381         if $self->paystart_year
382            and $self->paystart_year < 1990;
383
384       return "Issue number must be beween 1 and 99"
385         if $self->payissue
386           and $self->payissue < 1 || $self->payissue > 99;
387
388     } else {
389       $self->paystart_month('');
390       $self->paystart_year('');
391       $self->payissue('');
392     }
393
394   } elsif ( !$ignore_invalid_card && 
395     $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
396
397     my $payinfo = $self->payinfo;
398     $payinfo =~ s/[^\d\@\.]//g;
399     if ( $conf->config('echeck-country') eq 'CA' ) {
400       $payinfo =~ /^(\d+)\@(\d{5})\.(\d{3})$/
401         or return 'invalid echeck account@branch.bank';
402       $payinfo = "$1\@$2.$3";
403     } elsif ( $conf->config('echeck-country') eq 'US' ) {
404       $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
405       $payinfo = "$1\@$2";
406     } else {
407       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@routing';
408       $payinfo = "$1\@$2";
409     }
410     $self->payinfo($payinfo);
411     $self->paycvv('');
412
413     unless ( $ignore_banned_card ) {
414       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
415       if ( $ban ) {
416         if ( $ban->bantype eq 'warn' ) {
417           #or others depending on value of $ban->reason ?
418           return '_duplicate_ach' unless $self->override_ban_warn;
419         } else {
420           return 'Banned ACH account: banned on '.
421                  time2str('%a %h %o at %r', $ban->_date).
422                  ' by '. $ban->otaker.
423                  ' (ban# '. $ban->bannum. ')';
424         }
425       }
426     }
427
428 #  } elsif ( $self->payby eq 'PREPAY' ) {
429 #
430 #    my $payinfo = $self->payinfo;
431 #    $payinfo =~ s/\W//g; #anything else would just confuse things
432 #    $self->payinfo($payinfo);
433 #    $error = $self->ut_alpha('payinfo');
434 #    return "Illegal prepayment identifier: ". $self->payinfo if $error;
435 #    return "Unknown prepayment identifier"
436 #      unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
437 #    $self->paycvv('');
438
439   }
440
441   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
442
443     $self->paydate('');
444
445   } elsif ( $self->payby =~ /^(CARD|DCRD)$/ ) {
446
447     # shouldn't payinfo_check do this?
448     return "Expiration date required"
449       if $self->paydate eq '' || $self->paydate eq '-';
450
451     my( $m, $y );
452     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
453       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
454     } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
455       ( $m, $y ) = ( $2, "19$1" );
456     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
457       ( $m, $y ) = ( $3, "20$2" );
458     } else {
459       return "Illegal expiration date: ". $self->paydate;
460     }
461     $m = sprintf('%02d',$m);
462     $self->paydate("$y-$m-01");
463     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
464     return gettext('expired_card')
465       if #XXX !$import
466       #&&
467          !$ignore_expired_card 
468       && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
469
470   }
471
472   if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
473        ( ! $conf->exists('require_cardname')
474          || $self->payby !~ /^(CARD|DCRD)$/  ) 
475   ) {
476     $self->payname( $self->first. " ". $self->getfield('last') );
477   } else {
478
479     if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
480       $self->payname =~ /^([\w \,\.\-\']*)$/
481         or return gettext('illegal_name'). " payname: ". $self->payname;
482       $self->payname($1);
483     } else {
484       $self->payname =~ /^([\w \,\.\-\'\&]*)$/
485         or return gettext('illegal_name'). " payname: ". $self->payname;
486       $self->payname($1);
487     }
488
489   }
490
491   ###
492
493   $self->SUPER::check;
494 }
495
496 sub check_payinfo_cardtype {
497   my $self = shift;
498
499   return '' unless $self->payby =~ /^(CARD|CHEK)$/;
500
501   my $payinfo = $self->payinfo;
502   $payinfo =~ s/\D//g;
503
504   return '' if $payinfo =~ /^99\d{14}$/; #token
505
506   my %bop_card_types = map { $_=>1 } values %{ card_types() };
507   my $cardtype = cardtype($payinfo);
508
509   return "$cardtype not accepted" unless $bop_card_types{$cardtype};
510
511   '';
512
513 }
514
515 sub _banned_pay_hashref {
516   my $self = shift;
517
518   my %payby2ban = (
519     'CARD' => 'CARD',
520     'DCRD' => 'CARD',
521     'CHEK' => 'CHEK',
522     'DCHK' => 'CHEK'
523   );
524
525   {
526     'payby'   => $payby2ban{$self->payby},
527     'payinfo' => $self->payinfo,
528     #don't ever *search* on reason! #'reason'  =>
529   };
530 }
531
532 sub _new_banned_pay_hashref {
533   my $self = shift;
534   my $hr = $self->_banned_pay_hashref;
535   $hr->{payinfo_hash} = 'SHA512';
536   $hr->{payinfo} = sha512_base64($hr->{payinfo});
537   $hr;
538 }
539
540 =item paydate_mon_year
541
542 Returns a two element list consisting of the paydate month and year.
543
544 =cut
545
546 sub paydate_mon_year {
547   my $self = shift;
548
549   my $date = $self->paydate; # || '12-2037';
550
551   #false laziness w/elements/select-month_year.html
552   if ( $date  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
553     ( $2, $1 );
554   } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
555     ( $1, $3 );
556   } else {
557     warn "unrecognized expiration date format: $date";
558     ( '', '' );
559   }
560
561 }
562
563 =item label
564
565 Returns a one line text label for this payment type.
566
567 =cut
568
569 my %weight = (
570   1 => 'Primary',
571   2 => 'Secondary',
572   3 => 'Tertiary',
573   4 => 'Fourth',
574   5 => 'Fifth',
575   6 => 'Sixth',
576   7 => 'Seventh',
577 );
578
579 sub label {
580   my $self = shift;
581
582   my $name = $self->payby =~ /^(CARD|DCRD)$/
583               && cardtype($self->paymask) || FS::payby->shortname($self->payby);
584
585   ( $self->payby =~ /^(CARD|CHEK)$/  ? $weight{$self->weight}. ' automatic '
586                                      : 'Manual '
587   ).
588   "$name: ". $self->paymask.
589   ( $self->payby =~ /^(CARD|DCRD)$/
590       ? ' Exp '. join('/', $self->paydate_mon_year)
591       : ''
592   );
593
594 }
595
596 =item realtime_bop
597
598 =cut
599
600 sub realtime_bop {
601   my( $self, %opt ) = @_;
602
603   $opt{$_} = $self->$_() for qw( payinfo payname paydate );
604
605   if ( $self->locationnum ) {
606     my $cust_location = $self->cust_location;
607     $opt{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
608   }
609
610   $self->cust_main->realtime_bop({
611     'method' => FS::payby->payby2bop( $self->payby ),
612     %opt,
613   });
614
615 }
616
617 =item paytypes
618
619 Returns a list of valid values for the paytype field (bank account type for
620 electronic check payment).
621
622 =cut
623
624 sub paytypes {
625   #my $class = shift;
626
627   ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
628 }
629
630 =item cgi_cust_payby_fields
631
632 Returns the field names used in the web interface (including some pseudo-fields).
633
634 =cut
635
636 sub cgi_cust_payby_fields {
637   #my $class = shift;
638   [qw( payby payinfo paydate_month paydate_year paycvv payname weight
639        payinfo1 payinfo2 payinfo3 paytype paystate payname_CHEK )];
640 }
641
642 =item cgi_hash_callback HASHREF OLD
643
644 Subroutine (not a class or object method).  Processes a hash reference
645 of web interface contet (transfers the data from pseudo-fields to real fields).
646
647 If OLD object is passed, also preserves locationnum, paystart_month, paystart_year,
648 payissue and payip.  If the new field is blank but the old is not, the old field 
649 will be preserved.
650
651 =cut
652
653 sub cgi_hash_callback {
654   my $hashref = shift;
655   my $old = shift;
656
657   my %noauto = (
658     'CARD' => 'DCRD',
659     'CHEK' => 'DCHK',
660   );
661   $hashref->{payby} = $noauto{$hashref->{payby}}
662     if ! $hashref->{weight} && exists $noauto{$hashref->{payby}};
663
664   if ( $hashref->{payby} =~ /^(CHEK|DCHK)$/ ) {
665
666     unless ( grep $hashref->{$_}, qw(payinfo1 payinfo2 payinfo3 payname_CHEK)) {
667       %$hashref = ();
668       return;
669     }
670
671     $hashref->{payinfo} = $hashref->{payinfo1}. '@';
672     $hashref->{payinfo} .= $hashref->{payinfo3}.'.' 
673       if $conf->config('echeck-country') eq 'CA';
674     $hashref->{payinfo} .= $hashref->{'payinfo2'};
675
676     $hashref->{payname} = $hashref->{'payname_CHEK'};
677
678   } elsif ( $hashref->{payby} =~ /^(CARD|DCRD)$/ ) {
679
680     unless ( grep $hashref->{$_}, qw( payinfo paycvv payname ) ) {
681       %$hashref = ();
682       return;
683     }
684
685   }
686
687   $hashref->{paydate}= $hashref->{paydate_month}. '-'. $hashref->{paydate_year};
688
689   if ($old) {
690     foreach my $field ( qw(locationnum paystart_month paystart_year payissue payip) ) {
691       next if $hashref->{$field};
692       next unless $old->get($field);
693       $hashref->{$field} = $old->get($field);
694     }
695   }
696
697 }
698
699 =item search_sql
700
701 Class method.
702
703 Returns a qsearch hash expression to search for parameters specified in HASHREF.
704 Valid paramters are:
705
706 =over 4
707
708 =item payby
709
710 listref
711
712 =item paydate_year
713
714 =item paydate_month
715
716
717 =back
718
719 =cut
720
721 sub search_sql {
722   my ($class, $params) = @_;
723
724   my @where = ();
725   my $orderby;
726
727   # initialize these to prevent warnings
728   $params = {
729     'paydate_year'  => '',
730     %$params
731   };
732
733   ###
734   # payby
735   ###
736
737   if ( $params->{'payby'} ) {
738
739     my @payby = ref( $params->{'payby'} )
740                   ? @{ $params->{'payby'} }
741                   :  ( $params->{'payby'} );
742
743     @payby = grep /^([A-Z]{4})$/, @payby;
744     my $in_payby = 'IN(' . join(',', map {"'$_'"} @payby) . ')';
745     push @where, "cust_payby.payby $in_payby"
746       if @payby;
747   }
748
749   ###
750   # paydate_year / paydate_month
751   ###
752
753   if ( $params->{'paydate_year'} =~ /^(\d{4})$/ ) {
754     my $year = $1;
755     $params->{'paydate_month'} =~ /^(\d\d?)$/
756       or die "paydate_year without paydate_month?";
757     my $month = $1;
758
759     push @where,
760       'cust_payby.paydate IS NOT NULL',
761       "cust_payby.paydate != ''",
762       "CAST(cust_payby.paydate AS timestamp) < CAST('$year-$month-01' AS timestamp )"
763 ;
764   }
765   ##
766   # setup queries, subs, etc. for the search
767   ##
768
769   $orderby ||= 'ORDER BY custnum';
770
771   # here is the agent virtualization
772   push @where,
773     $FS::CurrentUser::CurrentUser->agentnums_sql(table => 'cust_main');
774
775   my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
776
777   my $addl_from = ' LEFT JOIN cust_main USING ( custnum ) ';
778   # always make address fields available in results
779   for my $pre ('bill_', 'ship_') {
780     $addl_from .= 
781       ' LEFT JOIN cust_location AS '.$pre.'location '.
782       'ON (cust_main.'.$pre.'locationnum = '.$pre.'location.locationnum) ';
783   }
784
785   my $count_query = "SELECT COUNT(*) FROM cust_payby $addl_from $extra_sql";
786
787   my @select = ( 'cust_payby.*',
788                  #'cust_main.custnum',
789                  # there's a good chance that we'll need these
790                  'cust_main.bill_locationnum',
791                  'cust_main.ship_locationnum',
792                  FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
793                );
794
795   my $select = join(', ', @select);
796
797   my $sql_query = {
798     'table'         => 'cust_payby',
799     'select'        => $select,
800     'addl_from'     => $addl_from,
801     'hashref'       => {},
802     'extra_sql'     => $extra_sql,
803     'order_by'      => $orderby,
804     'count_query'   => $count_query,
805   };
806   $sql_query;
807
808 }
809
810 =back
811
812 =head1 BUGS
813
814 =head1 SEE ALSO
815
816 L<FS::Record>, schema.html from the base documentation.
817
818 =cut
819
820 1;
821