1 #this is so kludgy i'd be embarassed if it wasn't cybercash's fault
3 use vars qw($paymentserversecret $paymentserverport $paymentserverhost);
8 use vars qw( @ISA $conf $lpr $processor $xaction $E_NoErr $invoice_from
17 use Business::CreditCard;
18 use FS::UID qw( getotaker );
19 use FS::Record qw( qsearchs qsearch );
22 use FS::cust_bill_pkg;
25 use FS::cust_pay_batch;
26 use FS::part_referral;
27 use FS::cust_main_county;
29 use FS::cust_main_invoice;
31 @ISA = qw( FS::Record );
33 #ask FS::UID to run this stuff for us later
34 $FS::UID::callback{'FS::cust_main'} = sub {
36 $lpr = $conf->config('lpr');
37 $invoice_from = $conf->config('invoice_from');
38 $smtpmachine = $conf->config('smtpmachine');
40 if ( $conf->exists('cybercash3.2') ) {
42 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
43 require CCMckDirectLib3_2;
45 require CCMckErrno3_2;
46 #qw(MCKGetErrorMessage $E_NoErr);
47 import CCMckErrno3_2 qw($E_NoErr);
50 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
51 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
52 if ( $status != $E_NoErr ) {
53 warn "CCMckLib3_2::InitConfig error:\n";
54 foreach my $key (keys %CCMckLib3_2::Config) {
55 warn " $key => $CCMckLib3_2::Config{$key}\n"
57 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
58 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
60 $processor='cybercash3.2';
61 } elsif ( $conf->exists('cybercash2') ) {
64 ( $main::paymentserverhost,
65 $main::paymentserverport,
66 $main::paymentserversecret,
68 ) = $conf->config('cybercash2');
69 $processor='cybercash2';
75 FS::cust_main - Object methods for cust_main records
81 $record = new FS::cust_main \%hash;
82 $record = new FS::cust_main { 'column' => 'value' };
84 $error = $record->insert;
86 $error = $new_record->replace($old_record);
88 $error = $record->delete;
90 $error = $record->check;
92 @cust_pkg = $record->all_pkgs;
94 @cust_pkg = $record->ncancelled_pkgs;
96 $error = $record->bill;
97 $error = $record->bill %options;
98 $error = $record->bill 'time' => $time;
100 $error = $record->collect;
101 $error = $record->collect %options;
102 $error = $record->collect 'invoice_time' => $time,
103 'batch_card' => 'yes',
104 'report_badcard' => 'yes',
109 An FS::cust_main object represents a customer. FS::cust_main inherits from
110 FS::Record. The following fields are currently supported:
114 =item custnum - primary key (assigned automatically for new customers)
116 =item agentnum - agent (see L<FS::agent>)
118 =item refnum - referral (see L<FS::part_referral>)
124 =item ss - social security number (optional)
126 =item company - (optional)
130 =item address2 - (optional)
134 =item county - (optional, see L<FS::cust_main_county>)
136 =item state - (see L<FS::cust_main_county>)
140 =item country - (see L<FS::cust_main_county>)
142 =item daytime - phone (optional)
144 =item night - phone (optional)
146 =item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
148 =item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
150 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
152 =item payname - name on card or billing name
154 =item tax - tax exempt, empty or `Y'
156 =item otaker - order taker (assigned automatically, see L<FS::UID>)
166 Creates a new customer. To add the customer to the database, see L<"insert">.
168 Note that this stores the hash reference, not a distinct copy of the hash it
169 points to. You can ask the object for a copy with the I<hash> method.
173 sub table { 'cust_main'; }
177 Adds this customer to the database. If there is an error, returns the error,
178 otherwise returns false.
182 Currently unimplemented. Maybe cancel all of this customer's
185 I don't remove the customer record in the database because there would then
186 be no record the customer ever existed (which is bad, no?)
191 return "Can't (yet?) delete customers.";
194 =item replace OLD_RECORD
196 Replaces the OLD_RECORD with this one in the database. If there is an error,
197 returns the error, otherwise returns false.
201 Checks all fields to make sure this is a valid customer record. If there is
202 an error, returns the error, otherwise returns false. Called by the insert
211 $self->ut_numbern('custnum')
212 || $self->ut_number('agentnum')
213 || $self->ut_number('refnum')
214 || $self->ut_textn('company')
215 || $self->ut_text('address1')
216 || $self->ut_textn('address2')
217 || $self->ut_text('city')
218 || $self->ut_textn('county')
219 || $self->ut_text('state')
220 || $self->ut_phonen('daytime')
221 || $self->ut_phonen('night')
222 || $self->ut_phonen('fax')
224 return $error if $error;
226 return "Unknown agent"
227 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
229 return "Unknown referral"
230 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
232 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/
233 or return "Illegal last name: ". $self->getfield('last');
234 $self->setfield('last',$1);
236 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
239 if ( $self->ss eq '' ) {
244 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
245 or return "Illegal social security number";
246 $self->ss("$1-$2-$3");
249 $self->country =~ /^(\w\w)$/ or return "Illegal country";
251 unless ( qsearchs('cust_main_county', {
252 'country' => $self->country,
255 return "Unknown state/county/country"
256 #" state ". $self->state. " county ". $self->county. " country ". $self->country
257 unless qsearchs('cust_main_county',{
258 'state' => $self->state,
259 'county' => $self->county,
260 'country' => $self->country,
264 $self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/ or return "Illegal zip";
267 $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby: ". $self->payby;
270 if ( $self->payby eq 'CARD' ) {
272 my $payinfo = $self->payinfo;
274 $payinfo =~ /^(\d{13,16})$/
275 or return "Illegal credit card number";
277 $self->payinfo($payinfo);
278 validate($payinfo) or return "Illegal credit card number";
279 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
281 } elsif ( $self->payby eq 'BILL' ) {
283 $error = $self->ut_textn('payinfo');
284 return "Illegal P.O. number" if $error;
286 } elsif ( $self->payby eq 'COMP' ) {
288 $error = $self->ut_textn('payinfo');
289 return "Illegal comp account issuer" if $error;
293 if ( $self->paydate eq '' ) {
294 return "Expriation date required" unless $self->payby eq 'BILL';
297 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
298 or return "Illegal expiration date";
299 if ( length($2) == 4 ) {
300 $self->paydate("$2-$1-01");
301 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
302 $self->paydate("19$2-$1-01");
304 $self->paydate("20$2-$1-01");
308 if ( $self->payname eq '' ) {
309 $self->payname( $self->first. " ". $self->getfield('last') );
311 $self->payname =~ /^([\w \,\.\-\']+)$/
312 or return "Illegal billing name";
316 $self->tax =~ /^(Y?)$/ or return "Illegal tax";
319 $self->otaker(getotaker);
326 Returns all packages (see L<FS::cust_pkg>) for this customer.
332 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
335 =item ncancelled_pkgs
337 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
341 sub ncancelled_pkgs {
343 qsearch( 'cust_pkg', {
344 'custnum' => $self->custnum,
351 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
352 conjunction with the collect method.
354 The only currently available option is `time', which bills the customer as if
355 it were that time. It is specified as a UNIX timestamp; see
356 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
359 If there is an error, returns the error, otherwise returns false.
364 my( $self, %options ) = @_;
365 my $time = $options{'time'} || time;
370 local $SIG{HUP} = 'IGNORE';
371 local $SIG{INT} = 'IGNORE';
372 local $SIG{QUIT} = 'IGNORE';
373 local $SIG{TERM} = 'IGNORE';
374 local $SIG{TSTP} = 'IGNORE';
375 local $SIG{PIPE} = 'IGNORE';
377 # find the packages which are due for billing, find out how much they are
378 # & generate invoice database.
380 my( $total_setup, $total_recur ) = ( 0, 0 );
383 foreach my $cust_pkg (
384 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
387 next if $cust_pkg->getfield('cancel');
389 #? to avoid use of uninitialized value errors... ?
390 $cust_pkg->setfield('bill', '')
391 unless defined($cust_pkg->bill);
393 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
395 #so we don't modify cust_pkg record unnecessarily
396 my $cust_pkg_mod_flag = 0;
397 my %hash = $cust_pkg->hash;
398 my $old_cust_pkg = new FS::cust_pkg \%hash;
402 unless ( $cust_pkg->setup ) {
403 my $setup_prog = $part_pkg->getfield('setup');
405 #$cpt->permit(); #what is necessary?
406 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
407 $setup = $cpt->reval($setup_prog);
408 unless ( defined($setup) ) {
409 warn "Error reval-ing part_pkg->setup pkgpart ",
410 $part_pkg->pkgpart, ": $@";
412 $cust_pkg->setfield('setup',$time);
413 $cust_pkg_mod_flag=1;
420 if ( $part_pkg->getfield('freq') > 0 &&
421 ! $cust_pkg->getfield('susp') &&
422 ( $cust_pkg->getfield('bill') || 0 ) < $time
424 my $recur_prog = $part_pkg->getfield('recur');
426 #$cpt->permit(); #what is necessary?
427 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
428 $recur = $cpt->reval($recur_prog);
429 unless ( defined($recur) ) {
430 warn "Error reval-ing part_pkg->recur pkgpart ",
431 $part_pkg->pkgpart, ": $@";
433 #change this bit to use Date::Manip?
434 #$sdate=$cust_pkg->bill || time;
435 #$sdate=$cust_pkg->bill || $time;
436 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
437 my ($sec,$min,$hour,$mday,$mon,$year) =
438 (localtime($sdate) )[0,1,2,3,4,5];
439 $mon += $part_pkg->getfield('freq');
440 until ( $mon < 12 ) { $mon -= 12; $year++; }
441 $cust_pkg->setfield('bill',
442 timelocal($sec,$min,$hour,$mday,$mon,$year));
443 $cust_pkg_mod_flag = 1;
447 warn "setup is undefinded" unless defined($setup);
448 warn "recur is undefinded" unless defined($recur);
449 warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
451 if ( $cust_pkg_mod_flag ) {
452 $error=$cust_pkg->replace($old_cust_pkg);
453 if ( $error ) { #just in case
454 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
456 $setup = sprintf( "%.2f", $setup );
457 $recur = sprintf( "%.2f", $recur );
458 my $cust_bill_pkg = new FS::cust_bill_pkg ({
459 'pkgnum' => $cust_pkg->pkgnum,
463 'edate' => $cust_pkg->bill,
465 push @cust_bill_pkg, $cust_bill_pkg;
466 $total_setup += $setup;
467 $total_recur += $recur;
473 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
475 return '' if scalar(@cust_bill_pkg) == 0;
477 unless ( $self->getfield('tax') =~ /Y/i
478 || $self->getfield('payby') eq 'COMP'
480 my $cust_main_county = qsearchs('cust_main_county',{
481 'state' => $self->state,
482 'county' => $self->county,
483 'country' => $self->country,
485 my $tax = sprintf( "%.2f",
486 $charged * ( $cust_main_county->getfield('tax') / 100 )
488 $charged = sprintf( "%.2f", $charged+$tax );
490 my $cust_bill_pkg = new FS::cust_bill_pkg ({
497 push @cust_bill_pkg, $cust_bill_pkg;
500 my $cust_bill = new FS::cust_bill ( {
501 'custnum' => $self->getfield('custnum'),
503 'charged' => $charged,
505 $error = $cust_bill->insert;
506 #shouldn't happen, but how else to handle this? (wrap me in eval, to catch
508 die "Error creating cust_bill record: $error!\n",
509 "Check updated but unbilled packages for customer", $self->custnum, "\n"
512 my $invnum = $cust_bill->invnum;
514 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
515 $cust_bill_pkg->setfield( 'invnum', $invnum );
516 $error = $cust_bill_pkg->insert;
517 #shouldn't happen, but how else tohandle this?
518 die "Error creating cust_bill_pkg record: $error!\n",
519 "Check incomplete invoice ", $invnum, "\n"
526 =item collect OPTIONS
528 (Attempt to) collect money for this customer's outstanding invoices (see
529 L<FS::cust_bill>). Usually used after the bill method.
531 Depending on the value of `payby', this may print an invoice (`BILL'), charge
532 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
534 If there is an error, returns the error, otherwise returns false.
536 Currently available options are:
538 invoice_time - Use this time when deciding when to print invoices and
539 late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse>
540 for conversion functions.
542 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
543 default, cards are processed immediately, which will generate an error if
544 CyberCash is not installed.
546 report_badcard - Set this true if you want bad card transactions to
547 return an error. By default, they don't.
552 my( $self, %options ) = @_;
553 my $invoice_time = $options{'invoice_time'} || time;
555 my $total_owed = $self->balance;
556 return '' unless $total_owed > 0; #redundant?????
559 local $SIG{HUP} = 'IGNORE';
560 local $SIG{INT} = 'IGNORE';
561 local $SIG{QUIT} = 'IGNORE';
562 local $SIG{TERM} = 'IGNORE';
563 local $SIG{TSTP} = 'IGNORE';
564 local $SIG{PIPE} = 'IGNORE';
566 foreach my $cust_bill (
567 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
570 #this has to be before next's
571 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
575 $total_owed = sprintf( "%.2f", $total_owed - $amount );
577 next unless $cust_bill->owed > 0;
579 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
581 #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
583 next unless $amount > 0;
585 if ( $self->payby eq 'BILL' ) {
588 my $since = $invoice_time - ( $cust_bill->_date || 0 );
589 #warn "$invoice_time ", $cust_bill->_date, " $since";
590 if ( $since >= 0 #don't print future invoices
591 && ( $cust_bill->printed * 2592000 ) <= $since
594 #my @print_text = $cust_bill->print_text; #( date )
595 my @invoicing_list = $self->invoicing_list;
596 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
597 $ENV{SMTPHOSTS} = $smtpmachine;
598 $ENV{MAILADDRESS} = $invoice_from;
599 my $header = new Mail::Header ( [
600 "From: $invoice_from",
601 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
602 "Sender: $invoice_from",
603 "Reply-To: $invoice_from",
604 "Date: ". time2str("%a, %d %b %Y %X %z", time),
607 my $message = new Mail::Internet (
609 'Body' => [ $cust_bill->print_text ], #( date)
611 $message->smtpsend or die "Can't send invoice email!"; #die? warn?
613 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
614 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
615 print LPR $cust_bill->print_text; #( date )
617 or die $! ? "Error closing $lpr: $!"
618 : "Exit status $? from $lpr";
621 my %hash = $cust_bill->hash;
623 my $new_cust_bill = new FS::cust_bill(\%hash);
624 my $error = $new_cust_bill->replace($cust_bill);
625 warn "Error updating $cust_bill->printed: $error" if $error;
629 } elsif ( $self->payby eq 'COMP' ) {
630 my $cust_pay = new FS::cust_pay ( {
631 'invnum' => $cust_bill->invnum,
635 'payinfo' => $self->payinfo,
638 my $error = $cust_pay->insert;
639 return 'Error COMPing invnum #' . $cust_bill->invnum .
640 ':' . $error if $error;
642 } elsif ( $self->payby eq 'CARD' ) {
644 if ( $options{'batch_card'} ne 'yes' ) {
646 return "Real time card processing not enabled!" unless $processor;
648 if ( $processor =~ /^cybercash/ ) {
650 #fix exp. date for cybercash
651 #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
652 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
655 my $paybatch = $cust_bill->invnum.
656 '-' . time2str("%y%m%d%H%M%S", time);
658 my $payname = $self->payname ||
659 $self->getfield('first'). ' '. $self->getfield('last');
661 my $address = $self->address1;
662 $address .= ", ". $self->address2 if $self->address2;
664 my $country = 'USA' if $self->country eq 'US';
666 my @full_xaction = ( $xaction,
667 'Order-ID' => $paybatch,
668 'Amount' => "usd $amount",
669 'Card-Number' => $self->getfield('payinfo'),
670 'Card-Name' => $payname,
671 'Card-Address' => $address,
672 'Card-City' => $self->getfield('city'),
673 'Card-State' => $self->getfield('state'),
674 'Card-Zip' => $self->getfield('zip'),
675 'Card-Country' => $country,
680 if ( $processor eq 'cybercash2' ) {
681 $^W=0; #CCLib isn't -w safe, ugh!
682 %result = &CCLib::sendmserver(@full_xaction);
684 } elsif ( $processor eq 'cybercash3.2' ) {
685 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
687 return "Unkonwn real-time processor $processor\n";
690 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
691 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
692 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
693 my $cust_pay = new FS::cust_pay ( {
694 'invnum' => $cust_bill->invnum,
698 'payinfo' => $self->payinfo,
699 'paybatch' => "$processor:$paybatch",
701 my $error = $cust_pay->insert;
702 return 'Error applying payment, invnum #' .
703 $cust_bill->invnum. ':'. $error if $error;
704 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
705 || $options{'report_badcard'} ) {
706 return 'Cybercash error, invnum #' .
707 $cust_bill->invnum. ':'. $result{'MErrMsg'};
713 return "Unkonwn real-time processor $processor\n";
718 my $cust_pay_batch = new FS::Record ('cust_pay_batch', {
719 'invnum' => $cust_bill->getfield('invnum'),
720 'custnum' => $self->getfield('custnum'),
721 'last' => $self->getfield('last'),
722 'first' => $self->getfield('first'),
723 'address1' => $self->getfield('address1'),
724 'address2' => $self->getfield('address2'),
725 'city' => $self->getfield('city'),
726 'state' => $self->getfield('state'),
727 'zip' => $self->getfield('zip'),
728 'country' => $self->getfield('country'),
730 'cardnum' => $self->getfield('payinfo'),
731 'exp' => $self->getfield('paydate'),
732 'payname' => $self->getfield('payname'),
735 my $error = $cust_pay_batch->insert;
736 return "Error adding to cust_pay_batch: $error" if $error;
741 return "Unknown payment type ". $self->payby;
755 Returns the total owed for this customer on all invoices
756 (see L<FS::cust_bill>).
763 foreach my $cust_bill ( qsearch('cust_bill', {
764 'custnum' => $self->custnum,
766 $total_bill += $cust_bill->owed;
768 sprintf( "%.2f", $total_bill );
773 Returns the total credits (see L<FS::cust_credit>) for this customer.
779 my $total_credit = 0;
780 foreach my $cust_credit ( qsearch('cust_credit', {
781 'custnum' => $self->custnum,
783 $total_credit += $cust_credit->credited;
785 sprintf( "%.2f", $total_credit );
790 Returns the balance for this customer (total owed minus total credited).
796 sprintf( "%.2f", $self->total_owed - $self->total_credited );
799 =item invoicing_list [ ARRAYREF ]
801 If an arguement is given, sets these email addresses as invoice recipients
802 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
803 (except as warnings), so use check_invoicing_list first.
805 Returns a list of email addresses (with svcnum entries expanded).
807 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
808 check it without disturbing anything by passing nothing.
810 This interface may change in the future.
815 my( $self, $arrayref ) = @_;
817 my @cust_main_invoice;
818 if ( $self->custnum ) {
820 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
822 @cust_main_invoice = ();
824 foreach my $cust_main_invoice ( @cust_main_invoice ) {
825 #warn $cust_main_invoice->destnum;
826 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
827 #warn $cust_main_invoice->destnum;
828 my $error = $cust_main_invoice->delete;
829 warn $error if $error;
832 if ( $self->custnum ) {
834 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
836 @cust_main_invoice = ();
838 foreach my $address ( @{$arrayref} ) {
839 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
840 my $cust_main_invoice = new FS::cust_main_invoice ( {
841 'custnum' => $self->custnum,
844 my $error = $cust_main_invoice->insert;
845 warn $error if $error;
849 if ( $self->custnum ) {
851 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
857 =item check_invoicing_list ARRAYREF
859 Checks these arguements as valid input for the invoicing_list method. If there
860 is an error, returns the error, otherwise returns false.
864 sub check_invoicing_list {
865 my( $self, $arrayref ) = @_;
866 foreach my $address ( @{$arrayref} ) {
867 my $cust_main_invoice = new FS::cust_main_invoice ( {
868 'custnum' => $self->custnum,
871 my $error = $self->custnum
872 ? $cust_main_invoice->check
873 : $cust_main_invoice->checkdest
875 return $error if $error;
884 $Id: cust_main.pm,v 1.18 1999-04-10 06:54:11 ivan Exp $
890 Bill and collect options should probably be passed as references instead of a
893 CyberCash v2 forces us to define some variables in package main.
895 There should probably be a configuration file with a list of allowed credit
898 CyberCash is the only processor.
900 No multiple currency support (probably a larger project than just this module).
904 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
905 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
906 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
907 L<FS::UID>, schema.html from the base documentation.
911 ivan@voicenet.com 97-jul-28
913 Changed to standard Business::CreditCard
915 EXPORT_OK FS::Record's hfields
916 removed unique calls and locking (not needed here now)
917 wrapped the (now) optional fields in if statements in sub check (notyetdone!)
918 ivan@sisd.com 97-nov-12
920 updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
922 Added export of datasrc from UID.pm for Pg6.3
923 changed 'day' to 'daytime' because Pg6.3 reserves the day word
924 bmccane@maxbaud.net 98-apr-3
926 in ->create, s/svc_acct/cust_main/, now it should actually eliminate the
927 warnings it was meant to ivan@sisd.com 98-jul-16
929 don't require a phone number and allow '/' in company names
930 ivan@sisd.com 98-jul-18
932 use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
934 pod, merge with FS::Bill (about time!), total_owed, total_credited and balance
935 methods, cleaned collect method, source modifications no longer necessary to
936 enable cybercash, cybercash v3 support, don't need to import
937 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
939 $Log: cust_main.pm,v $
940 Revision 1.18 1999-04-10 06:54:11 ivan
943 Revision 1.17 1999/04/10 05:27:38 ivan
944 display an illegal payby, to assist importing
946 Revision 1.16 1999/04/07 14:32:19 ivan
947 more &invoicing_list logic to skip searches when there is no custnum
949 Revision 1.15 1999/04/07 13:41:54 ivan
950 in &invoicing_list, don't search if there's no custnum yet
952 Revision 1.14 1999/03/29 12:06:15 ivan
953 buglet in email invoices fixed
955 Revision 1.13 1999/02/28 20:09:03 ivan
956 allow spaces in zip codes, for (at least) canada. pointed out by
957 Clayton Gray <clgray@bcgroup.net>
959 Revision 1.12 1999/02/27 21:24:22 ivan
960 parse paydate correctly for cybercash
962 Revision 1.11 1999/02/23 08:09:27 ivan
963 beginnings of one-screen new customer entry and some other miscellania
965 Revision 1.10 1999/01/25 12:26:09 ivan
966 yet more mod_perl stuff
968 Revision 1.9 1999/01/18 09:22:41 ivan
969 changes to track email addresses for email invoicing
971 Revision 1.8 1998/12/29 11:59:39 ivan
972 mostly properly OO, some work still to be done with svc_ stuff
974 Revision 1.7 1998/12/16 09:58:52 ivan
975 library support for editing email invoice destinations (not in sub collect yet)
977 Revision 1.6 1998/11/18 09:01:42 ivan
980 Revision 1.5 1998/11/15 11:23:14 ivan
981 use FS::table_name for all searches to eliminate warnings,
982 emit state/county when they don't match
984 Revision 1.4 1998/11/15 05:30:48 ivan
985 bugfix for new config layout
987 Revision 1.3 1998/11/13 09:56:54 ivan
988 change configuration file layout to support multiple distinct databases (with
989 own set of config files, export, etc.)
991 Revision 1.2 1998/11/07 10:24:25 ivan
992 don't use depriciated FS::Bill and FS::Invoice, other miscellania