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_number('agentnum')
207 || $self->ut_number('refnum')
208 || $self->ut_textn('company')
209 || $self->ut_text('address1')
210 || $self->ut_textn('address2')
211 || $self->ut_text('city')
212 || $self->ut_textn('county')
213 || $self->ut_text('state')
214 || $self->ut_phonen('daytime')
215 || $self->ut_phonen('night')
216 || $self->ut_phonen('fax')
218 return $error if $error;
220 return "Unknown agent"
221 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
223 return "Unknown referral"
224 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
226 $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
227 $self->setfield('last',$1);
229 $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
232 if ( $self->ss eq '' ) {
237 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
238 or return "Illegal social security number";
239 $self->ss("$1-$2-$3");
242 $self->country =~ /^(\w\w)$/ or return "Illegal country";
244 unless ( qsearchs('cust_main_county', {
245 'country' => $self->country,
248 return "Unknown state/county/country"
249 #" state ". $self->state. " county ". $self->county. " country ". $self->country
250 unless qsearchs('cust_main_county',{
251 'state' => $self->state,
252 'county' => $self->county,
253 'country' => $self->country,
257 $self->zip =~ /^([\w\-]{10})$/ or return "Illegal zip";
260 $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby";
263 if ( $self->payby eq 'CARD' ) {
265 my $payinfo = $self->payinfo;
267 $payinfo =~ /^(\d{13,16})$/
268 or return "Illegal credit card number";
270 $self->payinfo($payinfo);
271 validate($payinfo) or return "Illegal credit card number";
272 return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
274 } elsif ( $self->payby eq 'BILL' ) {
276 $error = $self->ut_textn('payinfo');
277 return "Illegal P.O. number" if $error;
279 } elsif ( $self->payby eq 'COMP' ) {
281 $error = $self->ut_textn('payinfo');
282 return "Illegal comp account issuer" if $error;
286 if ( $self->paydate eq '' ) {
287 return "Expriation date required" unless $self->payby eq 'BILL';
290 $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
291 or return "Illegal expiration date";
292 if ( length($2) == 4 ) {
293 $self->paydate("$2-$1-01");
294 } elsif ( $2 > 97 ) { #should pry change to check for "this year"
295 $self->paydate("19$2-$1-01");
297 $self->paydate("20$2-$1-01");
301 if ( $self->payname eq '' ) {
302 $self->payname( $self->first. " ". $self->getfield('last') );
304 $self->payname =~ /^([\w \,\.\-\']+)$/
305 or return "Illegal billing name";
309 $self->tax =~ /^(Y?)$/ or return "Illegal tax";
312 $self->otaker(getotaker);
319 Returns all packages (see L<FS::cust_pkg>) for this customer.
325 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
328 =item ncancelled_pkgs
330 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
334 sub ncancelled_pkgs {
336 qsearch( 'cust_pkg', {
337 'custnum' => $self->custnum,
344 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
345 conjunction with the collect method.
347 The only currently available option is `time', which bills the customer as if
348 it were that time. It is specified as a UNIX timestamp; see
349 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
352 If there is an error, returns the error, otherwise returns false.
357 my( $self, %options ) = @_;
358 my $time = $options{'time'} || $^T;
363 local $SIG{HUP} = 'IGNORE';
364 local $SIG{INT} = 'IGNORE';
365 local $SIG{QUIT} = 'IGNORE';
366 local $SIG{TERM} = 'IGNORE';
367 local $SIG{TSTP} = 'IGNORE';
369 # find the packages which are due for billing, find out how much they are
370 # & generate invoice database.
372 my( $total_setup, $total_recur ) = ( 0, 0 );
375 foreach my $cust_pkg (
376 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
379 next if $cust_pkg->getfield('cancel');
381 #? to avoid use of uninitialized value errors... ?
382 $cust_pkg->setfield('bill', '')
383 unless defined($cust_pkg->bill);
385 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
387 #so we don't modify cust_pkg record unnecessarily
388 my $cust_pkg_mod_flag = 0;
389 my %hash = $cust_pkg->hash;
390 my $old_cust_pkg = new FS::cust_pkg \%hash;
394 unless ( $cust_pkg->setup ) {
395 my $setup_prog = $part_pkg->getfield('setup');
397 #$cpt->permit(); #what is necessary?
398 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
399 $setup = $cpt->reval($setup_prog);
400 unless ( defined($setup) ) {
401 warn "Error reval-ing part_pkg->setup pkgpart ",
402 $part_pkg->pkgpart, ": $@";
404 $cust_pkg->setfield('setup',$time);
405 $cust_pkg_mod_flag=1;
412 if ( $part_pkg->getfield('freq') > 0 &&
413 ! $cust_pkg->getfield('susp') &&
414 ( $cust_pkg->getfield('bill') || 0 ) < $time
416 my $recur_prog = $part_pkg->getfield('recur');
418 #$cpt->permit(); #what is necessary?
419 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
420 $recur = $cpt->reval($recur_prog);
421 unless ( defined($recur) ) {
422 warn "Error reval-ing part_pkg->recur pkgpart ",
423 $part_pkg->pkgpart, ": $@";
425 #change this bit to use Date::Manip?
426 #$sdate=$cust_pkg->bill || time;
427 #$sdate=$cust_pkg->bill || $time;
428 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
429 my ($sec,$min,$hour,$mday,$mon,$year) =
430 (localtime($sdate) )[0,1,2,3,4,5];
431 $mon += $part_pkg->getfield('freq');
432 until ( $mon < 12 ) { $mon -= 12; $year++; }
433 $cust_pkg->setfield('bill',
434 timelocal($sec,$min,$hour,$mday,$mon,$year));
435 $cust_pkg_mod_flag = 1;
439 warn "setup is undefinded" unless defined($setup);
440 warn "recur is undefinded" unless defined($recur);
441 warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
443 if ( $cust_pkg_mod_flag ) {
444 $error=$cust_pkg->replace($old_cust_pkg);
445 if ( $error ) { #just in case
446 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
448 $setup = sprintf( "%.2f", $setup );
449 $recur = sprintf( "%.2f", $recur );
450 my $cust_bill_pkg = new FS::cust_bill_pkg ({
451 'pkgnum' => $cust_pkg->pkgnum,
455 'edate' => $cust_pkg->bill,
457 push @cust_bill_pkg, $cust_bill_pkg;
458 $total_setup += $setup;
459 $total_recur += $recur;
465 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
467 return '' if scalar(@cust_bill_pkg) == 0;
469 unless ( $self->getfield('tax') =~ /Y/i
470 || $self->getfield('payby') eq 'COMP'
472 my $cust_main_county = qsearchs('cust_main_county',{
473 'state' => $self->state,
474 'county' => $self->county,
475 'country' => $self->country,
477 my $tax = sprintf( "%.2f",
478 $charged * ( $cust_main_county->getfield('tax') / 100 )
480 $charged = sprintf( "%.2f", $charged+$tax );
482 my $cust_bill_pkg = new FS::cust_bill_pkg ({
489 push @cust_bill_pkg, $cust_bill_pkg;
492 my $cust_bill = new FS::cust_bill ( {
493 'custnum' => $self->getfield('custnum'),
495 'charged' => $charged,
497 $error = $cust_bill->insert;
498 #shouldn't happen, but how else to handle this? (wrap me in eval, to catch
500 die "Error creating cust_bill record: $error!\n",
501 "Check updated but unbilled packages for customer", $self->custnum, "\n"
504 my $invnum = $cust_bill->invnum;
506 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
507 $cust_bill_pkg->setfield( 'invnum', $invnum );
508 $error = $cust_bill_pkg->insert;
509 #shouldn't happen, but how else tohandle this?
510 die "Error creating cust_bill_pkg record: $error!\n",
511 "Check incomplete invoice ", $invnum, "\n"
518 =item collect OPTIONS
520 (Attempt to) collect money for this customer's outstanding invoices (see
521 L<FS::cust_bill>). Usually used after the bill method.
523 Depending on the value of `payby', this may print an invoice (`BILL'), charge
524 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
526 If there is an error, returns the error, otherwise returns false.
528 Currently available options are:
530 invoice_time - Use this time when deciding when to print invoices and
531 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>
532 for conversion functions.
534 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
535 default, cards are processed immediately, which will generate an error if
536 CyberCash is not installed.
538 report_badcard - Set this true if you want bad card transactions to
539 return an error. By default, they don't.
544 my( $self, %options ) = @_;
545 my $invoice_time = $options{'invoice_time'} || $^T;
547 my $total_owed = $self->balance;
548 return '' unless $total_owed > 0; #redundant?????
551 local $SIG{HUP} = 'IGNORE';
552 local $SIG{INT} = 'IGNORE';
553 local $SIG{QUIT} = 'IGNORE';
554 local $SIG{TERM} = 'IGNORE';
555 local $SIG{TSTP} = 'IGNORE';
557 foreach my $cust_bill (
558 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
561 #this has to be before next's
562 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
566 $total_owed = sprintf( "%.2f", $total_owed - $amount );
568 next unless $cust_bill->owed > 0;
570 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
572 #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
574 next unless $amount > 0;
576 if ( $self->payby eq 'BILL' ) {
579 my $since = $invoice_time - ( $cust_bill->_date || 0 );
580 #warn "$invoice_time ", $cust_bill->_date, " $since";
581 if ( $since >= 0 #don't print future invoices
582 && ( $cust_bill->printed * 2592000 ) <= $since
585 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
586 print LPR $cust_bill->print_text; #( date )
588 or die $! ? "Error closing $lpr: $!"
589 : "Exit status $? from $lpr";
591 my %hash = $cust_bill->hash;
593 my $new_cust_bill = new FS::cust_bill(\%hash);
594 my $error = $new_cust_bill->replace($cust_bill);
595 warn "Error updating $cust_bill->printed: $error" if $error;
599 } elsif ( $self->payby eq 'COMP' ) {
600 my $cust_pay = new FS::cust_pay ( {
601 'invnum' => $cust_bill->invnum,
605 'payinfo' => $self->payinfo,
608 my $error = $cust_pay->insert;
609 return 'Error COMPing invnum #' . $cust_bill->invnum .
610 ':' . $error if $error;
611 } elsif ( $self->payby eq 'CARD' ) {
613 if ( $options{'batch_card'} ne 'yes' ) {
615 return "Real time card processing not enabled!" unless $processor;
617 if ( $processor =~ /^cybercash/ ) {
619 #fix exp. date for cybercash
620 $self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
623 my $paybatch = $cust_bill->invnum.
624 '-' . time2str("%y%m%d%H%M%S", time);
626 my $payname = $self->payname ||
627 $self->getfield('first'). ' '. $self->getfield('last');
629 my $address = $self->address1;
630 $address .= ", ". $self->address2 if $self->address2;
632 my $country = 'USA' if $self->country eq 'US';
634 my @full_xaction = ( $xaction,
635 'Order-ID' => $paybatch,
636 'Amount' => "usd $amount",
637 'Card-Number' => $self->getfield('payinfo'),
638 'Card-Name' => $payname,
639 'Card-Address' => $address,
640 'Card-City' => $self->getfield('city'),
641 'Card-State' => $self->getfield('state'),
642 'Card-Zip' => $self->getfield('zip'),
643 'Card-Country' => $country,
648 if ( $processor eq 'cybercash2' ) {
649 $^W=0; #CCLib isn't -w safe, ugh!
650 %result = &CCLib::sendmserver(@full_xaction);
652 } elsif ( $processor eq 'cybercash3.2' ) {
653 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
655 return "Unkonwn real-time processor $processor\n";
658 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
659 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
660 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
661 my $cust_pay = new FS::cust_pay ( {
662 'invnum' => $cust_bill->invnum,
666 'payinfo' => $self->payinfo,
667 'paybatch' => "$processor:$paybatch",
669 my $error = $cust_pay->insert;
670 return 'Error applying payment, invnum #' .
671 $cust_bill->invnum. ':'. $error if $error;
672 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
673 || $options{'report_badcard'} ) {
674 return 'Cybercash error, invnum #' .
675 $cust_bill->invnum. ':'. $result{'MErrMsg'};
681 return "Unkonwn real-time processor $processor\n";
686 my $cust_pay_batch = new FS::Record ('cust_pay_batch', {
687 'invnum' => $cust_bill->getfield('invnum'),
688 'custnum' => $self->getfield('custnum'),
689 'last' => $self->getfield('last'),
690 'first' => $self->getfield('first'),
691 'address1' => $self->getfield('address1'),
692 'address2' => $self->getfield('address2'),
693 'city' => $self->getfield('city'),
694 'state' => $self->getfield('state'),
695 'zip' => $self->getfield('zip'),
696 'country' => $self->getfield('country'),
698 'cardnum' => $self->getfield('payinfo'),
699 'exp' => $self->getfield('paydate'),
700 'payname' => $self->getfield('payname'),
703 my $error = $cust_pay_batch->insert;
704 return "Error adding to cust_pay_batch: $error" if $error;
709 return "Unknown payment type ". $self->payby;
719 Returns the total owed for this customer on all invoices
720 (see L<FS::cust_bill>).
727 foreach my $cust_bill ( qsearch('cust_bill', {
728 'custnum' => $self->custnum,
730 $total_bill += $cust_bill->owed;
732 sprintf( "%.2f", $total_bill );
737 Returns the total credits (see L<FS::cust_credit>) for this customer.
743 my $total_credit = 0;
744 foreach my $cust_credit ( qsearch('cust_credit', {
745 'custnum' => $self->custnum,
747 $total_credit += $cust_credit->credited;
749 sprintf( "%.2f", $total_credit );
754 Returns the balance for this customer (total owed minus total credited).
760 sprintf( "%.2f", $self->total_owed - $self->total_credited );
763 =item invoicing_list [ ITEM, ITEM, ... ]
765 If arguements are given, sets these email addresses as invoice recipients
766 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
767 (except as warnings), so use check_invoicing_list first.
769 Returns a list of email addresses (with svcnum entries expanded).
774 my( $self, @addresses ) = @_;
776 my @cust_main_invoice =
777 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
778 foreach my $cust_main_invoice ( @cust_main_invoice ) {
779 unless ( grep { $cust_main_invoice->address eq $_ } @addresses ) {
780 $cust_main_invoice->delete;
784 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
785 foreach my $address ( @addresses ) {
786 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
787 my $cust_main_invoice = new FS::cust_main_invoice (
788 'custnum' => $self->custnum,
791 my $error = $cust_main_invoice->insert;
792 warn $error if $error;
797 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
800 =item check_invoicing_list ITEM, ITEM
802 Checks these arguements as valid input for the invoicing_list method. If there
803 is an error, returns the error, otherwise returns false.
807 sub check_invoicing_list {
808 my( $self, @addresses ) = @_;
809 foreach my $address ( @addresses ) {
810 my $cust_main_invoice = new FS::cust_main_invoice (
811 'custnum' => $self->custnum,
814 my $error = $cust_main_invoice->check;
815 return $error if $error;
824 $Id: cust_main.pm,v 1.8 1998-12-29 11:59:39 ivan Exp $
830 Bill and collect options should probably be passed as references instead of a
833 CyberCash v2 forces us to define some variables in package main.
835 There should probably be a configuration file with a list of allowed credit
838 CyberCash is the only processor.
840 No multiple currency support (probably a larger project than just this module).
844 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
845 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
846 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
847 L<FS::UID>, schema.html from the base documentation.
851 ivan@voicenet.com 97-jul-28
853 Changed to standard Business::CreditCard
855 EXPORT_OK FS::Record's hfields
856 removed unique calls and locking (not needed here now)
857 wrapped the (now) optional fields in if statements in sub check (notyetdone!)
858 ivan@sisd.com 97-nov-12
860 updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
862 Added export of datasrc from UID.pm for Pg6.3
863 changed 'day' to 'daytime' because Pg6.3 reserves the day word
864 bmccane@maxbaud.net 98-apr-3
866 in ->create, s/svc_acct/cust_main/, now it should actually eliminate the
867 warnings it was meant to ivan@sisd.com 98-jul-16
869 don't require a phone number and allow '/' in company names
870 ivan@sisd.com 98-jul-18
872 use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
874 pod, merge with FS::Bill (about time!), total_owed, total_credited and balance
875 methods, cleaned collect method, source modifications no longer necessary to
876 enable cybercash, cybercash v3 support, don't need to import
877 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
879 $Log: cust_main.pm,v $
880 Revision 1.8 1998-12-29 11:59:39 ivan
881 mostly properly OO, some work still to be done with svc_ stuff
883 Revision 1.7 1998/12/16 09:58:52 ivan
884 library support for editing email invoice destinations (not in sub collect yet)
886 Revision 1.6 1998/11/18 09:01:42 ivan
889 Revision 1.5 1998/11/15 11:23:14 ivan
890 use FS::table_name for all searches to eliminate warnings,
891 emit state/county when they don't match
893 Revision 1.4 1998/11/15 05:30:48 ivan
894 bugfix for new config layout
896 Revision 1.3 1998/11/13 09:56:54 ivan
897 change configuration file layout to support multiple distinct databases (with
898 own set of config files, export, etc.)
900 Revision 1.2 1998/11/07 10:24:25 ivan
901 don't use depriciated FS::Bill and FS::Invoice, other miscellania