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);
14 use Business::CreditCard;
15 use FS::UID qw( getotaker );
16 use FS::Record qw( qsearchs qsearch );
19 use FS::cust_bill_pkg;
22 use FS::cust_pay_batch;
23 use FS::part_referral;
24 use FS::cust_main_county;
26 use FS::cust_main_invoice;
28 @ISA = qw( FS::Record );
30 #ask FS::UID to run this stuff for us later
31 $FS::UID::callback{'FS::cust_main'} = sub {
33 $lpr = $conf->config('lpr');
35 if ( $conf->exists('cybercash3.2') ) {
37 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
38 require CCMckDirectLib3_2;
40 require CCMckErrno3_2;
41 #qw(MCKGetErrorMessage $E_NoErr);
42 import CCMckErrno3_2 qw($E_NoErr);
45 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
46 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
47 if ( $status != $E_NoErr ) {
48 warn "CCMckLib3_2::InitConfig error:\n";
49 foreach my $key (keys %CCMckLib3_2::Config) {
50 warn " $key => $CCMckLib3_2::Config{$key}\n"
52 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
53 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
55 $processor='cybercash3.2';
56 } elsif ( $conf->exists('cybercash2') ) {
59 ( $main::paymentserverhost,
60 $main::paymentserverport,
61 $main::paymentserversecret,
63 ) = $conf->config('cybercash2');
64 $processor='cybercash2';
70 FS::cust_main - Object methods for cust_main records
76 $record = new FS::cust_main \%hash;
77 $record = new FS::cust_main { 'column' => 'value' };
79 $error = $record->insert;
81 $error = $new_record->replace($old_record);
83 $error = $record->delete;
85 $error = $record->check;
87 @cust_pkg = $record->all_pkgs;
89 @cust_pkg = $record->ncancelled_pkgs;
91 $error = $record->bill;
92 $error = $record->bill %options;
93 $error = $record->bill 'time' => $time;
95 $error = $record->collect;
96 $error = $record->collect %options;
97 $error = $record->collect 'invoice_time' => $time,
98 'batch_card' => 'yes',
99 'report_badcard' => 'yes',
104 An FS::cust_main object represents a customer. FS::cust_main inherits from
105 FS::Record. The following fields are currently supported:
109 =item custnum - primary key (assigned automatically for new customers)
111 =item agentnum - agent (see L<FS::agent>)
113 =item refnum - referral (see L<FS::part_referral>)
119 =item ss - social security number (optional)
121 =item company - (optional)
125 =item address2 - (optional)
129 =item county - (optional, see L<FS::cust_main_county>)
131 =item state - (see L<FS::cust_main_county>)
135 =item country - (see L<FS::cust_main_county>)
137 =item daytime - phone (optional)
139 =item night - phone (optional)
141 =item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
143 =item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
145 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
147 =item payname - name on card or billing name
149 =item tax - tax exempt, empty or `Y'
151 =item otaker - order taker (assigned automatically, see L<FS::UID>)
161 Creates a new customer. To add the customer to the database, see L<"insert">.
163 Note that this stores the hash reference, not a distinct copy of the hash it
164 points to. You can ask the object for a copy with the I<hash> method.
168 sub table { 'cust_main'; }
172 Adds this customer to the database. If there is an error, returns the error,
173 otherwise returns false.
177 Currently unimplemented. Maybe cancel all of this customer's
180 I don't remove the customer record in the database because there would then
181 be no record the customer ever existed (which is bad, no?)
186 return "Can't (yet?) delete customers.";
189 =item replace OLD_RECORD
191 Replaces the OLD_RECORD with this one in the database. If there is an error,
192 returns the error, otherwise returns false.
196 Checks all fields to make sure this is a valid customer record. If there is
197 an error, returns the error, otherwise returns false. Called by the insert
206 $self->ut_numbern('custnum')
207 || $self->ut_number('agentnum')
208 || $self->ut_number('refnum')
209 || $self->ut_textn('company')
210 || $self->ut_text('address1')
211 || $self->ut_textn('address2')
212 || $self->ut_text('city')
213 || $self->ut_textn('county')
214 || $self->ut_text('state')
215 || $self->ut_phonen('daytime')
216 || $self->ut_phonen('night')
217 || $self->ut_phonen('fax')
219 return $error if $error;
221 return "Unknown agent"
222 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
224 return "Unknown referral"
225 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
227 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
228 $self->setfield('last',$1);
230 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
233 if ( $self->ss eq '' ) {
238 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
239 or return "Illegal social security number";
240 $self->ss("$1-$2-$3");
243 $self->country =~ /^(\w\w)$/ or return "Illegal country";
245 unless ( qsearchs('cust_main_county', {
246 'country' => $self->country,
249 return "Unknown state/county/country"
250 #" state ". $self->state. " county ". $self->county. " country ". $self->country
251 unless qsearchs('cust_main_county',{
252 'state' => $self->state,
253 'county' => $self->county,
254 'country' => $self->country,
258 $self->zip =~ /^([\w\-]{5,10})$/ or return "Illegal zip";
261 $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby";
264 if ( $self->payby eq 'CARD' ) {
266 my $payinfo = $self->payinfo;
268 $payinfo =~ /^(\d{13,16})$/
269 or return "Illegal credit card number";
271 $self->payinfo($payinfo);
272 validate($payinfo) or return "Illegal credit card number";
273 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
275 } elsif ( $self->payby eq 'BILL' ) {
277 $error = $self->ut_textn('payinfo');
278 return "Illegal P.O. number" if $error;
280 } elsif ( $self->payby eq 'COMP' ) {
282 $error = $self->ut_textn('payinfo');
283 return "Illegal comp account issuer" if $error;
287 if ( $self->paydate eq '' ) {
288 return "Expriation date required" unless $self->payby eq 'BILL';
291 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
292 or return "Illegal expiration date";
293 if ( length($2) == 4 ) {
294 $self->paydate("$2-$1-01");
295 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
296 $self->paydate("19$2-$1-01");
298 $self->paydate("20$2-$1-01");
302 if ( $self->payname eq '' ) {
303 $self->payname( $self->first. " ". $self->getfield('last') );
305 $self->payname =~ /^([\w \,\.\-\']+)$/
306 or return "Illegal billing name";
310 $self->tax =~ /^(Y?)$/ or return "Illegal tax";
313 $self->otaker(getotaker);
320 Returns all packages (see L<FS::cust_pkg>) for this customer.
326 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
329 =item ncancelled_pkgs
331 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
335 sub ncancelled_pkgs {
337 qsearch( 'cust_pkg', {
338 'custnum' => $self->custnum,
345 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
346 conjunction with the collect method.
348 The only currently available option is `time', which bills the customer as if
349 it were that time. It is specified as a UNIX timestamp; see
350 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
353 If there is an error, returns the error, otherwise returns false.
358 my( $self, %options ) = @_;
359 my $time = $options{'time'} || time;
364 local $SIG{HUP} = 'IGNORE';
365 local $SIG{INT} = 'IGNORE';
366 local $SIG{QUIT} = 'IGNORE';
367 local $SIG{TERM} = 'IGNORE';
368 local $SIG{TSTP} = 'IGNORE';
369 local $SIG{PIPE} = 'IGNORE';
371 # find the packages which are due for billing, find out how much they are
372 # & generate invoice database.
374 my( $total_setup, $total_recur ) = ( 0, 0 );
377 foreach my $cust_pkg (
378 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
381 next if $cust_pkg->getfield('cancel');
383 #? to avoid use of uninitialized value errors... ?
384 $cust_pkg->setfield('bill', '')
385 unless defined($cust_pkg->bill);
387 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
389 #so we don't modify cust_pkg record unnecessarily
390 my $cust_pkg_mod_flag = 0;
391 my %hash = $cust_pkg->hash;
392 my $old_cust_pkg = new FS::cust_pkg \%hash;
396 unless ( $cust_pkg->setup ) {
397 my $setup_prog = $part_pkg->getfield('setup');
399 #$cpt->permit(); #what is necessary?
400 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
401 $setup = $cpt->reval($setup_prog);
402 unless ( defined($setup) ) {
403 warn "Error reval-ing part_pkg->setup pkgpart ",
404 $part_pkg->pkgpart, ": $@";
406 $cust_pkg->setfield('setup',$time);
407 $cust_pkg_mod_flag=1;
414 if ( $part_pkg->getfield('freq') > 0 &&
415 ! $cust_pkg->getfield('susp') &&
416 ( $cust_pkg->getfield('bill') || 0 ) < $time
418 my $recur_prog = $part_pkg->getfield('recur');
420 #$cpt->permit(); #what is necessary?
421 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
422 $recur = $cpt->reval($recur_prog);
423 unless ( defined($recur) ) {
424 warn "Error reval-ing part_pkg->recur pkgpart ",
425 $part_pkg->pkgpart, ": $@";
427 #change this bit to use Date::Manip?
428 #$sdate=$cust_pkg->bill || time;
429 #$sdate=$cust_pkg->bill || $time;
430 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
431 my ($sec,$min,$hour,$mday,$mon,$year) =
432 (localtime($sdate) )[0,1,2,3,4,5];
433 $mon += $part_pkg->getfield('freq');
434 until ( $mon < 12 ) { $mon -= 12; $year++; }
435 $cust_pkg->setfield('bill',
436 timelocal($sec,$min,$hour,$mday,$mon,$year));
437 $cust_pkg_mod_flag = 1;
441 warn "setup is undefinded" unless defined($setup);
442 warn "recur is undefinded" unless defined($recur);
443 warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
445 if ( $cust_pkg_mod_flag ) {
446 $error=$cust_pkg->replace($old_cust_pkg);
447 if ( $error ) { #just in case
448 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
450 $setup = sprintf( "%.2f", $setup );
451 $recur = sprintf( "%.2f", $recur );
452 my $cust_bill_pkg = new FS::cust_bill_pkg ({
453 'pkgnum' => $cust_pkg->pkgnum,
457 'edate' => $cust_pkg->bill,
459 push @cust_bill_pkg, $cust_bill_pkg;
460 $total_setup += $setup;
461 $total_recur += $recur;
467 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
469 return '' if scalar(@cust_bill_pkg) == 0;
471 unless ( $self->getfield('tax') =~ /Y/i
472 || $self->getfield('payby') eq 'COMP'
474 my $cust_main_county = qsearchs('cust_main_county',{
475 'state' => $self->state,
476 'county' => $self->county,
477 'country' => $self->country,
479 my $tax = sprintf( "%.2f",
480 $charged * ( $cust_main_county->getfield('tax') / 100 )
482 $charged = sprintf( "%.2f", $charged+$tax );
484 my $cust_bill_pkg = new FS::cust_bill_pkg ({
491 push @cust_bill_pkg, $cust_bill_pkg;
494 my $cust_bill = new FS::cust_bill ( {
495 'custnum' => $self->getfield('custnum'),
497 'charged' => $charged,
499 $error = $cust_bill->insert;
500 #shouldn't happen, but how else to handle this? (wrap me in eval, to catch
502 die "Error creating cust_bill record: $error!\n",
503 "Check updated but unbilled packages for customer", $self->custnum, "\n"
506 my $invnum = $cust_bill->invnum;
508 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
509 $cust_bill_pkg->setfield( 'invnum', $invnum );
510 $error = $cust_bill_pkg->insert;
511 #shouldn't happen, but how else tohandle this?
512 die "Error creating cust_bill_pkg record: $error!\n",
513 "Check incomplete invoice ", $invnum, "\n"
520 =item collect OPTIONS
522 (Attempt to) collect money for this customer's outstanding invoices (see
523 L<FS::cust_bill>). Usually used after the bill method.
525 Depending on the value of `payby', this may print an invoice (`BILL'), charge
526 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
528 If there is an error, returns the error, otherwise returns false.
530 Currently available options are:
532 invoice_time - Use this time when deciding when to print invoices and
533 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>
534 for conversion functions.
536 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
537 default, cards are processed immediately, which will generate an error if
538 CyberCash is not installed.
540 report_badcard - Set this true if you want bad card transactions to
541 return an error. By default, they don't.
546 my( $self, %options ) = @_;
547 my $invoice_time = $options{'invoice_time'} || time;
549 my $total_owed = $self->balance;
550 return '' unless $total_owed > 0; #redundant?????
553 local $SIG{HUP} = 'IGNORE';
554 local $SIG{INT} = 'IGNORE';
555 local $SIG{QUIT} = 'IGNORE';
556 local $SIG{TERM} = 'IGNORE';
557 local $SIG{TSTP} = 'IGNORE';
558 local $SIG{PIPE} = 'IGNORE';
560 foreach my $cust_bill (
561 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
564 #this has to be before next's
565 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
569 $total_owed = sprintf( "%.2f", $total_owed - $amount );
571 next unless $cust_bill->owed > 0;
573 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
575 #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
577 next unless $amount > 0;
579 if ( $self->payby eq 'BILL' ) {
582 my $since = $invoice_time - ( $cust_bill->_date || 0 );
583 #warn "$invoice_time ", $cust_bill->_date, " $since";
584 if ( $since >= 0 #don't print future invoices
585 && ( $cust_bill->printed * 2592000 ) <= $since
588 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
589 print LPR $cust_bill->print_text; #( date )
591 or die $! ? "Error closing $lpr: $!"
592 : "Exit status $? from $lpr";
594 my %hash = $cust_bill->hash;
596 my $new_cust_bill = new FS::cust_bill(\%hash);
597 my $error = $new_cust_bill->replace($cust_bill);
598 warn "Error updating $cust_bill->printed: $error" if $error;
602 } elsif ( $self->payby eq 'COMP' ) {
603 my $cust_pay = new FS::cust_pay ( {
604 'invnum' => $cust_bill->invnum,
608 'payinfo' => $self->payinfo,
611 my $error = $cust_pay->insert;
612 return 'Error COMPing invnum #' . $cust_bill->invnum .
613 ':' . $error if $error;
614 } elsif ( $self->payby eq 'CARD' ) {
616 if ( $options{'batch_card'} ne 'yes' ) {
618 return "Real time card processing not enabled!" unless $processor;
620 if ( $processor =~ /^cybercash/ ) {
622 #fix exp. date for cybercash
623 $self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
626 my $paybatch = $cust_bill->invnum.
627 '-' . time2str("%y%m%d%H%M%S", time);
629 my $payname = $self->payname ||
630 $self->getfield('first'). ' '. $self->getfield('last');
632 my $address = $self->address1;
633 $address .= ", ". $self->address2 if $self->address2;
635 my $country = 'USA' if $self->country eq 'US';
637 my @full_xaction = ( $xaction,
638 'Order-ID' => $paybatch,
639 'Amount' => "usd $amount",
640 'Card-Number' => $self->getfield('payinfo'),
641 'Card-Name' => $payname,
642 'Card-Address' => $address,
643 'Card-City' => $self->getfield('city'),
644 'Card-State' => $self->getfield('state'),
645 'Card-Zip' => $self->getfield('zip'),
646 'Card-Country' => $country,
651 if ( $processor eq 'cybercash2' ) {
652 $^W=0; #CCLib isn't -w safe, ugh!
653 %result = &CCLib::sendmserver(@full_xaction);
655 } elsif ( $processor eq 'cybercash3.2' ) {
656 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
658 return "Unkonwn real-time processor $processor\n";
661 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
662 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
663 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
664 my $cust_pay = new FS::cust_pay ( {
665 'invnum' => $cust_bill->invnum,
669 'payinfo' => $self->payinfo,
670 'paybatch' => "$processor:$paybatch",
672 my $error = $cust_pay->insert;
673 return 'Error applying payment, invnum #' .
674 $cust_bill->invnum. ':'. $error if $error;
675 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
676 || $options{'report_badcard'} ) {
677 return 'Cybercash error, invnum #' .
678 $cust_bill->invnum. ':'. $result{'MErrMsg'};
684 return "Unkonwn real-time processor $processor\n";
689 my $cust_pay_batch = new FS::Record ('cust_pay_batch', {
690 'invnum' => $cust_bill->getfield('invnum'),
691 'custnum' => $self->getfield('custnum'),
692 'last' => $self->getfield('last'),
693 'first' => $self->getfield('first'),
694 'address1' => $self->getfield('address1'),
695 'address2' => $self->getfield('address2'),
696 'city' => $self->getfield('city'),
697 'state' => $self->getfield('state'),
698 'zip' => $self->getfield('zip'),
699 'country' => $self->getfield('country'),
701 'cardnum' => $self->getfield('payinfo'),
702 'exp' => $self->getfield('paydate'),
703 'payname' => $self->getfield('payname'),
706 my $error = $cust_pay_batch->insert;
707 return "Error adding to cust_pay_batch: $error" if $error;
712 return "Unknown payment type ". $self->payby;
722 Returns the total owed for this customer on all invoices
723 (see L<FS::cust_bill>).
730 foreach my $cust_bill ( qsearch('cust_bill', {
731 'custnum' => $self->custnum,
733 $total_bill += $cust_bill->owed;
735 sprintf( "%.2f", $total_bill );
740 Returns the total credits (see L<FS::cust_credit>) for this customer.
746 my $total_credit = 0;
747 foreach my $cust_credit ( qsearch('cust_credit', {
748 'custnum' => $self->custnum,
750 $total_credit += $cust_credit->credited;
752 sprintf( "%.2f", $total_credit );
757 Returns the balance for this customer (total owed minus total credited).
763 sprintf( "%.2f", $self->total_owed - $self->total_credited );
766 =item invoicing_list [ ARRAYREF ]
768 If an arguement is given, sets these email addresses as invoice recipients
769 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
770 (except as warnings), so use check_invoicing_list first.
772 Returns a list of email addresses (with svcnum entries expanded).
774 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
775 check it without disturbing anything by passing nothing.
777 This interface may change in the future.
782 my( $self, $arrayref ) = @_;
784 my @cust_main_invoice =
785 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
786 foreach my $cust_main_invoice ( @cust_main_invoice ) {
787 #warn $cust_main_invoice->destnum;
788 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
789 #warn $cust_main_invoice->destnum;
790 my $error = $cust_main_invoice->delete;
791 warn $error if $error;
795 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
796 foreach my $address ( @{$arrayref} ) {
797 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
798 my $cust_main_invoice = new FS::cust_main_invoice ( {
799 'custnum' => $self->custnum,
802 my $error = $cust_main_invoice->insert;
803 warn $error if $error;
808 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
811 =item check_invoicing_list ARRAYREF
813 Checks these arguements as valid input for the invoicing_list method. If there
814 is an error, returns the error, otherwise returns false.
818 sub check_invoicing_list {
819 my( $self, $arrayref ) = @_;
820 foreach my $address ( @{$arrayref} ) {
821 my $cust_main_invoice = new FS::cust_main_invoice ( {
822 'custnum' => $self->custnum,
825 my $error = $self->custnum
826 ? $cust_main_invoice->check
827 : $cust_main_invoice->checkdest
829 return $error if $error;
838 $Id: cust_main.pm,v 1.10 1999-01-25 12:26:09 ivan Exp $
844 Bill and collect options should probably be passed as references instead of a
847 CyberCash v2 forces us to define some variables in package main.
849 There should probably be a configuration file with a list of allowed credit
852 CyberCash is the only processor.
854 No multiple currency support (probably a larger project than just this module).
858 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
859 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
860 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
861 L<FS::UID>, schema.html from the base documentation.
865 ivan@voicenet.com 97-jul-28
867 Changed to standard Business::CreditCard
869 EXPORT_OK FS::Record's hfields
870 removed unique calls and locking (not needed here now)
871 wrapped the (now) optional fields in if statements in sub check (notyetdone!)
872 ivan@sisd.com 97-nov-12
874 updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
876 Added export of datasrc from UID.pm for Pg6.3
877 changed 'day' to 'daytime' because Pg6.3 reserves the day word
878 bmccane@maxbaud.net 98-apr-3
880 in ->create, s/svc_acct/cust_main/, now it should actually eliminate the
881 warnings it was meant to ivan@sisd.com 98-jul-16
883 don't require a phone number and allow '/' in company names
884 ivan@sisd.com 98-jul-18
886 use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
888 pod, merge with FS::Bill (about time!), total_owed, total_credited and balance
889 methods, cleaned collect method, source modifications no longer necessary to
890 enable cybercash, cybercash v3 support, don't need to import
891 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
893 $Log: cust_main.pm,v $
894 Revision 1.10 1999-01-25 12:26:09 ivan
895 yet more mod_perl stuff
897 Revision 1.9 1999/01/18 09:22:41 ivan
898 changes to track email addresses for email invoicing
900 Revision 1.8 1998/12/29 11:59:39 ivan
901 mostly properly OO, some work still to be done with svc_ stuff
903 Revision 1.7 1998/12/16 09:58:52 ivan
904 library support for editing email invoice destinations (not in sub collect yet)
906 Revision 1.6 1998/11/18 09:01:42 ivan
909 Revision 1.5 1998/11/15 11:23:14 ivan
910 use FS::table_name for all searches to eliminate warnings,
911 emit state/county when they don't match
913 Revision 1.4 1998/11/15 05:30:48 ivan
914 bugfix for new config layout
916 Revision 1.3 1998/11/13 09:56:54 ivan
917 change configuration file layout to support multiple distinct databases (with
918 own set of config files, export, etc.)
920 Revision 1.2 1998/11/07 10:24:25 ivan
921 don't use depriciated FS::Bill and FS::Invoice, other miscellania