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'} || $^T;
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';
370 # find the packages which are due for billing, find out how much they are
371 # & generate invoice database.
373 my( $total_setup, $total_recur ) = ( 0, 0 );
376 foreach my $cust_pkg (
377 qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
380 next if $cust_pkg->getfield('cancel');
382 #? to avoid use of uninitialized value errors... ?
383 $cust_pkg->setfield('bill', '')
384 unless defined($cust_pkg->bill);
386 my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
388 #so we don't modify cust_pkg record unnecessarily
389 my $cust_pkg_mod_flag = 0;
390 my %hash = $cust_pkg->hash;
391 my $old_cust_pkg = new FS::cust_pkg \%hash;
395 unless ( $cust_pkg->setup ) {
396 my $setup_prog = $part_pkg->getfield('setup');
398 #$cpt->permit(); #what is necessary?
399 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
400 $setup = $cpt->reval($setup_prog);
401 unless ( defined($setup) ) {
402 warn "Error reval-ing part_pkg->setup pkgpart ",
403 $part_pkg->pkgpart, ": $@";
405 $cust_pkg->setfield('setup',$time);
406 $cust_pkg_mod_flag=1;
413 if ( $part_pkg->getfield('freq') > 0 &&
414 ! $cust_pkg->getfield('susp') &&
415 ( $cust_pkg->getfield('bill') || 0 ) < $time
417 my $recur_prog = $part_pkg->getfield('recur');
419 #$cpt->permit(); #what is necessary?
420 $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
421 $recur = $cpt->reval($recur_prog);
422 unless ( defined($recur) ) {
423 warn "Error reval-ing part_pkg->recur pkgpart ",
424 $part_pkg->pkgpart, ": $@";
426 #change this bit to use Date::Manip?
427 #$sdate=$cust_pkg->bill || time;
428 #$sdate=$cust_pkg->bill || $time;
429 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
430 my ($sec,$min,$hour,$mday,$mon,$year) =
431 (localtime($sdate) )[0,1,2,3,4,5];
432 $mon += $part_pkg->getfield('freq');
433 until ( $mon < 12 ) { $mon -= 12; $year++; }
434 $cust_pkg->setfield('bill',
435 timelocal($sec,$min,$hour,$mday,$mon,$year));
436 $cust_pkg_mod_flag = 1;
440 warn "setup is undefinded" unless defined($setup);
441 warn "recur is undefinded" unless defined($recur);
442 warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
444 if ( $cust_pkg_mod_flag ) {
445 $error=$cust_pkg->replace($old_cust_pkg);
446 if ( $error ) { #just in case
447 warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
449 $setup = sprintf( "%.2f", $setup );
450 $recur = sprintf( "%.2f", $recur );
451 my $cust_bill_pkg = new FS::cust_bill_pkg ({
452 'pkgnum' => $cust_pkg->pkgnum,
456 'edate' => $cust_pkg->bill,
458 push @cust_bill_pkg, $cust_bill_pkg;
459 $total_setup += $setup;
460 $total_recur += $recur;
466 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
468 return '' if scalar(@cust_bill_pkg) == 0;
470 unless ( $self->getfield('tax') =~ /Y/i
471 || $self->getfield('payby') eq 'COMP'
473 my $cust_main_county = qsearchs('cust_main_county',{
474 'state' => $self->state,
475 'county' => $self->county,
476 'country' => $self->country,
478 my $tax = sprintf( "%.2f",
479 $charged * ( $cust_main_county->getfield('tax') / 100 )
481 $charged = sprintf( "%.2f", $charged+$tax );
483 my $cust_bill_pkg = new FS::cust_bill_pkg ({
490 push @cust_bill_pkg, $cust_bill_pkg;
493 my $cust_bill = new FS::cust_bill ( {
494 'custnum' => $self->getfield('custnum'),
496 'charged' => $charged,
498 $error = $cust_bill->insert;
499 #shouldn't happen, but how else to handle this? (wrap me in eval, to catch
501 die "Error creating cust_bill record: $error!\n",
502 "Check updated but unbilled packages for customer", $self->custnum, "\n"
505 my $invnum = $cust_bill->invnum;
507 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
508 $cust_bill_pkg->setfield( 'invnum', $invnum );
509 $error = $cust_bill_pkg->insert;
510 #shouldn't happen, but how else tohandle this?
511 die "Error creating cust_bill_pkg record: $error!\n",
512 "Check incomplete invoice ", $invnum, "\n"
519 =item collect OPTIONS
521 (Attempt to) collect money for this customer's outstanding invoices (see
522 L<FS::cust_bill>). Usually used after the bill method.
524 Depending on the value of `payby', this may print an invoice (`BILL'), charge
525 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
527 If there is an error, returns the error, otherwise returns false.
529 Currently available options are:
531 invoice_time - Use this time when deciding when to print invoices and
532 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>
533 for conversion functions.
535 batch_card - Set this true to batch cards (see L<cust_pay_batch>). By
536 default, cards are processed immediately, which will generate an error if
537 CyberCash is not installed.
539 report_badcard - Set this true if you want bad card transactions to
540 return an error. By default, they don't.
545 my( $self, %options ) = @_;
546 my $invoice_time = $options{'invoice_time'} || $^T;
548 my $total_owed = $self->balance;
549 return '' unless $total_owed > 0; #redundant?????
552 local $SIG{HUP} = 'IGNORE';
553 local $SIG{INT} = 'IGNORE';
554 local $SIG{QUIT} = 'IGNORE';
555 local $SIG{TERM} = 'IGNORE';
556 local $SIG{TSTP} = 'IGNORE';
558 foreach my $cust_bill (
559 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
562 #this has to be before next's
563 my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
567 $total_owed = sprintf( "%.2f", $total_owed - $amount );
569 next unless $cust_bill->owed > 0;
571 next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
573 #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
575 next unless $amount > 0;
577 if ( $self->payby eq 'BILL' ) {
580 my $since = $invoice_time - ( $cust_bill->_date || 0 );
581 #warn "$invoice_time ", $cust_bill->_date, " $since";
582 if ( $since >= 0 #don't print future invoices
583 && ( $cust_bill->printed * 2592000 ) <= $since
586 open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
587 print LPR $cust_bill->print_text; #( date )
589 or die $! ? "Error closing $lpr: $!"
590 : "Exit status $? from $lpr";
592 my %hash = $cust_bill->hash;
594 my $new_cust_bill = new FS::cust_bill(\%hash);
595 my $error = $new_cust_bill->replace($cust_bill);
596 warn "Error updating $cust_bill->printed: $error" if $error;
600 } elsif ( $self->payby eq 'COMP' ) {
601 my $cust_pay = new FS::cust_pay ( {
602 'invnum' => $cust_bill->invnum,
606 'payinfo' => $self->payinfo,
609 my $error = $cust_pay->insert;
610 return 'Error COMPing invnum #' . $cust_bill->invnum .
611 ':' . $error if $error;
612 } elsif ( $self->payby eq 'CARD' ) {
614 if ( $options{'batch_card'} ne 'yes' ) {
616 return "Real time card processing not enabled!" unless $processor;
618 if ( $processor =~ /^cybercash/ ) {
620 #fix exp. date for cybercash
621 $self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
624 my $paybatch = $cust_bill->invnum.
625 '-' . time2str("%y%m%d%H%M%S", time);
627 my $payname = $self->payname ||
628 $self->getfield('first'). ' '. $self->getfield('last');
630 my $address = $self->address1;
631 $address .= ", ". $self->address2 if $self->address2;
633 my $country = 'USA' if $self->country eq 'US';
635 my @full_xaction = ( $xaction,
636 'Order-ID' => $paybatch,
637 'Amount' => "usd $amount",
638 'Card-Number' => $self->getfield('payinfo'),
639 'Card-Name' => $payname,
640 'Card-Address' => $address,
641 'Card-City' => $self->getfield('city'),
642 'Card-State' => $self->getfield('state'),
643 'Card-Zip' => $self->getfield('zip'),
644 'Card-Country' => $country,
649 if ( $processor eq 'cybercash2' ) {
650 $^W=0; #CCLib isn't -w safe, ugh!
651 %result = &CCLib::sendmserver(@full_xaction);
653 } elsif ( $processor eq 'cybercash3.2' ) {
654 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
656 return "Unkonwn real-time processor $processor\n";
659 #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
660 #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
661 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
662 my $cust_pay = new FS::cust_pay ( {
663 'invnum' => $cust_bill->invnum,
667 'payinfo' => $self->payinfo,
668 'paybatch' => "$processor:$paybatch",
670 my $error = $cust_pay->insert;
671 return 'Error applying payment, invnum #' .
672 $cust_bill->invnum. ':'. $error if $error;
673 } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
674 || $options{'report_badcard'} ) {
675 return 'Cybercash error, invnum #' .
676 $cust_bill->invnum. ':'. $result{'MErrMsg'};
682 return "Unkonwn real-time processor $processor\n";
687 my $cust_pay_batch = new FS::Record ('cust_pay_batch', {
688 'invnum' => $cust_bill->getfield('invnum'),
689 'custnum' => $self->getfield('custnum'),
690 'last' => $self->getfield('last'),
691 'first' => $self->getfield('first'),
692 'address1' => $self->getfield('address1'),
693 'address2' => $self->getfield('address2'),
694 'city' => $self->getfield('city'),
695 'state' => $self->getfield('state'),
696 'zip' => $self->getfield('zip'),
697 'country' => $self->getfield('country'),
699 'cardnum' => $self->getfield('payinfo'),
700 'exp' => $self->getfield('paydate'),
701 'payname' => $self->getfield('payname'),
704 my $error = $cust_pay_batch->insert;
705 return "Error adding to cust_pay_batch: $error" if $error;
710 return "Unknown payment type ". $self->payby;
720 Returns the total owed for this customer on all invoices
721 (see L<FS::cust_bill>).
728 foreach my $cust_bill ( qsearch('cust_bill', {
729 'custnum' => $self->custnum,
731 $total_bill += $cust_bill->owed;
733 sprintf( "%.2f", $total_bill );
738 Returns the total credits (see L<FS::cust_credit>) for this customer.
744 my $total_credit = 0;
745 foreach my $cust_credit ( qsearch('cust_credit', {
746 'custnum' => $self->custnum,
748 $total_credit += $cust_credit->credited;
750 sprintf( "%.2f", $total_credit );
755 Returns the balance for this customer (total owed minus total credited).
761 sprintf( "%.2f", $self->total_owed - $self->total_credited );
764 =item invoicing_list [ ARRAYREF ]
766 If an arguement is given, sets these email addresses as invoice recipients
767 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
768 (except as warnings), so use check_invoicing_list first.
770 Returns a list of email addresses (with svcnum entries expanded).
772 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
773 check it without disturbing anything by passing nothing.
775 This interface may change in the future.
780 my( $self, $arrayref ) = @_;
782 my @cust_main_invoice =
783 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
784 foreach my $cust_main_invoice ( @cust_main_invoice ) {
785 #warn $cust_main_invoice->destnum;
786 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
787 #warn $cust_main_invoice->destnum;
788 my $error = $cust_main_invoice->delete;
789 warn $error if $error;
793 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
794 foreach my $address ( @{$arrayref} ) {
795 unless ( grep { $address eq $_->address } @cust_main_invoice ) {
796 my $cust_main_invoice = new FS::cust_main_invoice ( {
797 'custnum' => $self->custnum,
800 my $error = $cust_main_invoice->insert;
801 warn $error if $error;
806 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
809 =item check_invoicing_list ARRAYREF
811 Checks these arguements as valid input for the invoicing_list method. If there
812 is an error, returns the error, otherwise returns false.
816 sub check_invoicing_list {
817 my( $self, $arrayref ) = @_;
818 foreach my $address ( @{$arrayref} ) {
819 my $cust_main_invoice = new FS::cust_main_invoice ( {
820 'custnum' => $self->custnum,
823 my $error = $cust_main_invoice->check;
824 return $error if $error;
833 $Id: cust_main.pm,v 1.9 1999-01-18 09:22:41 ivan Exp $
839 Bill and collect options should probably be passed as references instead of a
842 CyberCash v2 forces us to define some variables in package main.
844 There should probably be a configuration file with a list of allowed credit
847 CyberCash is the only processor.
849 No multiple currency support (probably a larger project than just this module).
853 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
854 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
855 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
856 L<FS::UID>, schema.html from the base documentation.
860 ivan@voicenet.com 97-jul-28
862 Changed to standard Business::CreditCard
864 EXPORT_OK FS::Record's hfields
865 removed unique calls and locking (not needed here now)
866 wrapped the (now) optional fields in if statements in sub check (notyetdone!)
867 ivan@sisd.com 97-nov-12
869 updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
871 Added export of datasrc from UID.pm for Pg6.3
872 changed 'day' to 'daytime' because Pg6.3 reserves the day word
873 bmccane@maxbaud.net 98-apr-3
875 in ->create, s/svc_acct/cust_main/, now it should actually eliminate the
876 warnings it was meant to ivan@sisd.com 98-jul-16
878 don't require a phone number and allow '/' in company names
879 ivan@sisd.com 98-jul-18
881 use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
883 pod, merge with FS::Bill (about time!), total_owed, total_credited and balance
884 methods, cleaned collect method, source modifications no longer necessary to
885 enable cybercash, cybercash v3 support, don't need to import
886 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
888 $Log: cust_main.pm,v $
889 Revision 1.9 1999-01-18 09:22:41 ivan
890 changes to track email addresses for email invoicing
892 Revision 1.8 1998/12/29 11:59:39 ivan
893 mostly properly OO, some work still to be done with svc_ stuff
895 Revision 1.7 1998/12/16 09:58:52 ivan
896 library support for editing email invoice destinations (not in sub collect yet)
898 Revision 1.6 1998/11/18 09:01:42 ivan
901 Revision 1.5 1998/11/15 11:23:14 ivan
902 use FS::table_name for all searches to eliminate warnings,
903 emit state/county when they don't match
905 Revision 1.4 1998/11/15 05:30:48 ivan
906 bugfix for new config layout
908 Revision 1.3 1998/11/13 09:56:54 ivan
909 change configuration file layout to support multiple distinct databases (with
910 own set of config files, export, etc.)
912 Revision 1.2 1998/11/07 10:24:25 ivan
913 don't use depriciated FS::Bill and FS::Invoice, other miscellania