2 use base qw( FS::Password_Mixin
6 use vars qw( $skip_fuzzyfiles );
8 use Scalar::Util qw( blessed );
9 use FS::Record qw( qsearch qsearchs dbh );
11 use FS::contact_phone;
12 use FS::contact_email;
13 use FS::contact::Import;
15 use FS::phone_type; #for cgi_contact_fields
17 use FS::prospect_contact;
23 FS::contact - Object methods for contact records
29 $record = new FS::contact \%hash;
30 $record = new FS::contact { 'column' => 'value' };
32 $error = $record->insert;
34 $error = $new_record->replace($old_record);
36 $error = $record->delete;
38 $error = $record->check;
42 An FS::contact object represents an specific contact person for a prospect or
43 customer. FS::contact inherits from FS::Record. The following fields are
80 =item selfservice_access
86 =item _password_encoding
102 Creates a new contact. To add the contact to the database, see L<"insert">.
104 Note that this stores the hash reference, not a distinct copy of the hash it
105 points to. You can ask the object for a copy with the I<hash> method.
109 sub table { 'contact'; }
113 Adds this record to the database. If there is an error, returns the error,
114 otherwise returns false.
116 If the object has an C<emailaddress> field, L<FS::contact_email> records
117 will be created for each (comma-separated) email address in that field. If
118 any of these coincide with an existing email address, this contact will be
119 merged with the contact with that address.
121 Then, if the object has any fields named C<phonetypenumN> an
122 L<FS::contact_phone> record will be created for each of them. Those fields
123 should contain phone numbers of the appropriate types (where N is the key of
124 an L<FS::phone_type> record identifying the type of number: daytime, night,
127 After inserting the record, if the object has a 'custnum' or 'prospectnum'
128 field, an L<FS::cust_contact> or L<FS::prospect_contact> record will be
129 created to link the contact to the customer. The following fields will also
130 be included in that record, if they are set on the object:
141 local $SIG{INT} = 'IGNORE';
142 local $SIG{QUIT} = 'IGNORE';
143 local $SIG{TERM} = 'IGNORE';
144 local $SIG{TSTP} = 'IGNORE';
145 local $SIG{PIPE} = 'IGNORE';
147 my $oldAutoCommit = $FS::UID::AutoCommit;
148 local $FS::UID::AutoCommit = 0;
151 #save off and blank values that move to cust_contact / prospect_contact now
152 my $prospectnum = $self->prospectnum;
153 $self->prospectnum('');
154 my $custnum = $self->custnum;
158 for (qw( classnum comment selfservice_access invoice_dest message_dest)) {
159 $link_hash{$_} = $self->get($_);
164 ## check for an existing contact with this email address other than current customer
165 ## if found, just add that contact to cust_contact with link_hash credentials
166 ## as email can not be tied to two contacts.
168 my $existing_contact = '';
169 my @contact_emails = ();
170 my %contact_nums = ();
172 if ( $self->get('emailaddress') =~ /\S/ ) {
174 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
176 my $contact_email = qsearchs('contact_email', { emailaddress=>$email } );
177 unless ($contact_email) { push @contact_emails, $email; next; }
179 my $contact = $contact_email->contact;
180 if ($contact->contactnum eq $self->contactnum) {
181 push @contact_emails, $email;
184 $contact_nums{$contact->contactnum} = '1';
189 my $emails = join(' , ', @contact_emails);
190 $self->emailaddress($emails);
192 $no_new_contact = '1' unless $self->emailaddress;
197 $error = $self->SUPER::insert unless $no_new_contact;
200 $dbh->rollback if $oldAutoCommit;
204 $contact_nums{$self->contactnum} = '1' if $self->contactnum;
206 my $cust_contact = '';
207 # if $self->custnum was set, then the customer-specific properties
208 # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
209 # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
212 foreach my $contactnum (keys %contact_nums) {
213 my %hash = ( 'contactnum' => $contactnum,
214 'custnum' => $custnum,
216 $cust_contact = qsearchs('cust_contact', \%hash )
217 || new FS::cust_contact { %hash, %link_hash };
218 my $error = $cust_contact->custcontactnum ? $cust_contact->replace
219 : $cust_contact->insert;
221 $dbh->rollback if $oldAutoCommit;
227 if ( $prospectnum && !$no_new_contact) {
228 my %hash = ( 'contactnum' => $self->contactnum,
229 'prospectnum' => $prospectnum,
231 my $prospect_contact = qsearchs('prospect_contact', \%hash )
232 || new FS::prospect_contact { %hash, %link_hash };
234 $prospect_contact->prospectcontactnum ? $prospect_contact->replace
235 : $prospect_contact->insert;
237 $dbh->rollback if $oldAutoCommit;
242 unless ($no_new_contact) {
243 foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
244 keys %{ $self->hashref } ) {
245 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
246 my $phonetypenum = $1;
248 my %hash = ( 'contactnum' => $self->contactnum,
249 'phonetypenum' => $phonetypenum,
252 qsearchs('contact_phone', \%hash)
253 || new FS::contact_phone { %hash, _parse_phonestring($self->get($pf)) };
254 my $error = $contact_phone->contactphonenum ? $contact_phone->replace
255 : $contact_phone->insert;
257 $dbh->rollback if $oldAutoCommit;
263 if ( $self->get('emailaddress') =~ /\S/ ) {
265 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
267 'contactnum' => $self->contactnum,
268 'emailaddress' => $email,
270 unless ( qsearchs('contact_email', \%hash) ) {
271 my $contact_email = new FS::contact_email \%hash;
272 my $error = $contact_email->insert;
274 $dbh->rollback if $oldAutoCommit;
282 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
283 #warn " queueing fuzzyfiles update\n"
285 my $error = $self->queue_fuzzyfiles_update;
287 $dbh->rollback if $oldAutoCommit;
288 return "updating fuzzy search cache: $error";
292 if ( $link_hash{'selfservice_access'} eq 'R'
293 or ( $link_hash{'selfservice_access'}
295 && ! length($self->_password)
299 my $error = $self->send_reset_email( queue=>1 );
301 $dbh->rollback if $oldAutoCommit;
306 if ( $self->get('password') ) {
307 my $error = $self->is_password_allowed($self->get('password'))
308 || $self->change_password($self->get('password'));
310 $dbh->rollback if $oldAutoCommit;
315 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
323 Delete this record from the database.
330 local $SIG{HUP} = 'IGNORE';
331 local $SIG{INT} = 'IGNORE';
332 local $SIG{QUIT} = 'IGNORE';
333 local $SIG{TERM} = 'IGNORE';
334 local $SIG{TSTP} = 'IGNORE';
335 local $SIG{PIPE} = 'IGNORE';
337 my $oldAutoCommit = $FS::UID::AutoCommit;
338 local $FS::UID::AutoCommit = 0;
341 #got a prospetnum or custnum? delete the prospect_contact or cust_contact link
343 if ( $self->prospectnum ) {
344 my $prospect_contact = qsearchs('prospect_contact', {
345 'contactnum' => $self->contactnum,
346 'prospectnum' => $self->prospectnum,
348 my $error = $prospect_contact->delete;
350 $dbh->rollback if $oldAutoCommit;
355 # if $self->custnum was set, then we're removing the contact from this
357 if ( $self->custnum ) {
358 my $cust_contact = qsearchs('cust_contact', {
359 'contactnum' => $self->contactnum,
360 'custnum' => $self->custnum,
362 my $error = $cust_contact->delete;
364 $dbh->rollback if $oldAutoCommit;
369 # then, proceed with deletion only if the contact isn't attached to any other
370 # prospects or customers
372 #inefficient, but how many prospects/customers can a single contact be
373 # attached too? (and is removing them from one a common operation?)
374 if ( $self->prospect_contact || $self->cust_contact ) {
375 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
379 #proceed with deletion
381 foreach my $cust_pkg ( $self->cust_pkg ) {
382 $cust_pkg->contactnum('');
383 my $error = $cust_pkg->replace;
385 $dbh->rollback if $oldAutoCommit;
390 foreach my $object ( $self->contact_phone, $self->contact_email ) {
391 my $error = $object->delete;
393 $dbh->rollback if $oldAutoCommit;
398 my $error = $self->delete_password_history
399 || $self->SUPER::delete;
401 $dbh->rollback if $oldAutoCommit;
405 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
410 =item replace OLD_RECORD
412 Replaces the OLD_RECORD with this one in the database. If there is an error,
413 returns the error, otherwise returns false.
420 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
422 : $self->replace_old;
424 $self->$_( $self->$_ || $old->$_ ) for qw( _password _password_encoding );
426 local $SIG{INT} = 'IGNORE';
427 local $SIG{QUIT} = 'IGNORE';
428 local $SIG{TERM} = 'IGNORE';
429 local $SIG{TSTP} = 'IGNORE';
430 local $SIG{PIPE} = 'IGNORE';
432 my $oldAutoCommit = $FS::UID::AutoCommit;
433 local $FS::UID::AutoCommit = 0;
436 #save off and blank values that move to cust_contact / prospect_contact now
437 my $prospectnum = $self->prospectnum;
438 $self->prospectnum('');
439 my $custnum = $self->custnum;
440 $self->custnum(''); $old->custnum(''); # remove because now stored cust_contact
443 for (qw( classnum comment selfservice_access invoice_dest message_dest )) {
444 $link_hash{$_} = $self->get($_);
445 $old->$_(''); ##remove values from old record, causes problem with history
449 ## check for an existing contact with this email address other than current customer
450 ## if found, just add that contact to cust_contact with link_hash credentials
451 ## as email can not be tied to two contacts.
452 my @contact_emails = ();
453 my %contact_nums = ();
454 $contact_nums{$self->contactnum} = '1' if $self->contactnum;
455 if ( $self->get('emailaddress') =~ /\S/ ) {
457 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
459 my $contact_email = qsearchs('contact_email', { emailaddress=>$email } );
460 unless ($contact_email) { push @contact_emails, $email; next; }
462 my $contact = $contact_email->contact;
463 if ($contact->contactnum eq $self->contactnum) {
464 push @contact_emails, $email;
467 $contact_nums{$contact->contactnum} = '1';
472 ## were all emails duplicates? if so reset original emails
473 if (scalar @contact_emails < 1 && scalar (keys %contact_nums) > 1) {
474 foreach (qsearch('contact_email', {'contactnum' => $self->contactnum})) {
475 push @contact_emails, $_->emailaddress;
479 my $emails = join(' , ', @contact_emails);
480 $self->emailaddress($emails);
484 my $error = $self->SUPER::replace($old);
485 if ( $old->_password ne $self->_password ) {
486 $error ||= $self->insert_password_history;
489 $dbh->rollback if $oldAutoCommit;
493 my $cust_contact = '';
494 # if $self->custnum was set, then the customer-specific properties
495 # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
496 # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
500 foreach my $contactnum (keys %contact_nums) {
502 my %hash = ( 'contactnum' => $contactnum,
503 'custnum' => $custnum,
506 if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) {
507 $cust_contact->$_($link_hash{$_}) for keys %link_hash;
508 $error = $cust_contact->replace;
510 $cust_contact = new FS::cust_contact { %hash, %link_hash };
511 $error = $cust_contact->insert;
514 $dbh->rollback if $oldAutoCommit;
520 if ( $prospectnum ) {
521 my %hash = ( 'contactnum' => $self->contactnum,
522 'prospectnum' => $prospectnum,
525 if ( my $prospect_contact = qsearchs('prospect_contact', \%hash ) ) {
526 $prospect_contact->$_($link_hash{$_}) for keys %link_hash;
527 $error = $prospect_contact->replace;
529 my $prospect_contact = new FS::prospect_contact { %hash, %link_hash };
530 $error = $prospect_contact->insert;
533 $dbh->rollback if $oldAutoCommit;
538 foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
539 keys %{ $self->hashref } ) {
540 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
541 my $phonetypenum = $1;
543 my %cp = ( 'contactnum' => $self->contactnum,
544 'phonetypenum' => $phonetypenum,
546 my $contact_phone = qsearchs('contact_phone', \%cp);
548 my $pv = $self->get($pf);
551 #if new value is empty, delete old entry
553 if ($contact_phone) {
554 $error = $contact_phone->delete;
556 $dbh->rollback if $oldAutoCommit;
563 $contact_phone ||= new FS::contact_phone \%cp;
565 my %cpd = _parse_phonestring( $pv );
566 $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
568 my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
570 $error = $contact_phone->$method;
572 $dbh->rollback if $oldAutoCommit;
577 if ( defined($self->hashref->{'emailaddress'}) ) {
579 my %contact_emails = ();
580 foreach my $contact_email ( $self->contact_email ) {
581 $contact_emails{$contact_email->emailaddress} = '1';
584 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
586 unless ($contact_emails{$email}) {
587 my $contact_email = new FS::contact_email {
588 'contactnum' => $self->contactnum,
589 'emailaddress' => $email,
591 $error = $contact_email->insert;
593 $dbh->rollback if $oldAutoCommit;
597 else { delete($contact_emails{$email}); }
601 foreach my $contact_email ( $self->contact_email ) {
602 if ($contact_emails{$contact_email->emailaddress}) {
603 my $error = $contact_email->delete;
605 $dbh->rollback if $oldAutoCommit;
613 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
614 #warn " queueing fuzzyfiles update\n"
616 $error = $self->queue_fuzzyfiles_update;
618 $dbh->rollback if $oldAutoCommit;
619 return "updating fuzzy search cache: $error";
623 if ( $cust_contact and (
624 ( $cust_contact->selfservice_access eq ''
625 && $link_hash{selfservice_access}
626 && ! length($self->_password)
628 || $cust_contact->_resend()
632 my $error = $self->send_reset_email( queue=>1 );
634 $dbh->rollback if $oldAutoCommit;
639 if ( $self->get('password') ) {
640 my $error = $self->is_password_allowed($self->get('password'))
641 || $self->change_password($self->get('password'));
643 $dbh->rollback if $oldAutoCommit;
648 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
654 =item _parse_phonestring PHONENUMBER_STRING
656 Subroutine, takes a string and returns a list (suitable for assigning to a hash)
657 with keys 'countrycode', 'phonenum' and 'extension'
659 (Should probably be moved to contact_phone.pm, hence the initial underscore.)
663 sub _parse_phonestring {
666 my($countrycode, $extension) = ('1', '');
669 if ( $value =~ s/^\s*\+\s*(\d+)// ) {
675 if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
679 ( 'countrycode' => $countrycode,
680 'phonenum' => $value,
681 'extension' => $extension,
685 =item queue_fuzzyfiles_update
687 Used by insert & replace to update the fuzzy search cache
691 use FS::cust_main::Search;
692 sub queue_fuzzyfiles_update {
695 local $SIG{HUP} = 'IGNORE';
696 local $SIG{INT} = 'IGNORE';
697 local $SIG{QUIT} = 'IGNORE';
698 local $SIG{TERM} = 'IGNORE';
699 local $SIG{TSTP} = 'IGNORE';
700 local $SIG{PIPE} = 'IGNORE';
702 my $oldAutoCommit = $FS::UID::AutoCommit;
703 local $FS::UID::AutoCommit = 0;
706 foreach my $field ( 'first', 'last' ) {
707 my $queue = new FS::queue {
708 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
710 my @args = "contact.$field", $self->get($field);
711 my $error = $queue->insert( @args );
713 $dbh->rollback if $oldAutoCommit;
714 return "queueing job (transaction rolled back): $error";
718 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
725 Checks all fields to make sure this is a valid contact. If there is
726 an error, returns the error, otherwise returns false. Called by the insert
734 if ( $self->selfservice_access eq 'R' || $self->selfservice_access eq 'P' ) {
735 $self->selfservice_access('Y');
740 $self->ut_numbern('contactnum')
741 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
742 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
743 || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
744 || $self->ut_foreign_keyn('classnum', 'contact_class', 'classnum')
745 || $self->ut_namen('last')
746 || $self->ut_namen('first')
747 || $self->ut_textn('title')
748 || $self->ut_textn('comment')
749 || $self->ut_enum('selfservice_access', [ '', 'Y' ])
750 || $self->ut_textn('_password')
751 || $self->ut_enum('_password_encoding', [ '', 'bcrypt'])
752 || $self->ut_enum('disabled', [ '', 'Y' ])
754 return $error if $error;
756 return "Prospect and customer!" if $self->prospectnum && $self->custnum;
758 return "One of first name, last name, or title must have a value"
759 if ! grep $self->$_(), qw( first last title);
766 Returns a formatted string representing this contact, including name, title and
773 my $data = $self->first. ' '. $self->last;
774 $data .= ', '. $self->title
776 $data .= ' ('. $self->comment. ')'
783 Returns a formatted string representing this contact, with just the name.
789 $self->first . ' ' . $self->last;
792 #=item contact_classname PROSPECT_OBJ | CUST_MAIN_OBJ
794 #Returns the name of this contact's class for the specified prospect or
795 #customer (see L<FS::prospect_contact>, L<FS::cust_contact> and
796 #L<FS::contact_class>).
800 #sub contact_classname {
801 # my( $self, $prospect_or_cust ) = @_;
804 # if ( ref($prospect_or_cust) eq 'FS::prospect_main' ) {
805 # $link = qsearchs('prospect_contact', {
806 # 'contactnum' => $self->contactnum,
807 # 'prospectnum' => $prospect_or_cust->prospectnum,
809 # } elsif ( ref($prospect_or_cust) eq 'FS::cust_main' ) {
810 # $link = qsearchs('cust_contact', {
811 # 'contactnum' => $self->contactnum,
812 # 'custnum' => $prospect_or_cust->custnum,
815 # croak "$prospect_or_cust is not an FS::prospect_main or FS::cust_main object";
818 # my $contact_class = $link->contact_class or return '';
819 # $contact_class->classname;
822 #autoloaded by FK in 4.x, but not during the upgrade
825 qsearch('contact_email', { 'contactnum' => $self->contactnum } );
828 =item by_selfservice_email EMAILADDRESS
830 Alternate search constructor (class method). Given an email address, returns
831 the contact for that address. If that contact doesn't have selfservice access,
832 or there isn't one, returns the empty string.
836 sub by_selfservice_email {
837 my($class, $email) = @_;
839 my $contact_email = qsearchs({
840 'table' => 'contact_email',
841 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
842 'hashref' => { 'emailaddress' => $email, },
844 AND ( contact.disabled IS NULL )
845 AND EXISTS ( SELECT 1 FROM cust_contact
846 WHERE contact.contactnum = cust_contact.contactnum
847 AND cust_contact.selfservice_access = 'Y'
852 $contact_email->contact;
856 #these three functions are very much false laziness w/FS/FS/Auth/internal.pm
857 # and should maybe be libraried in some way for other password needs
859 use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
861 sub authenticate_password {
862 my($self, $check_password) = @_;
864 if ( $self->_password_encoding eq 'bcrypt' ) {
866 my( $cost, $salt, $hash ) = split(',', $self->_password);
868 my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
870 salt => de_base64($salt),
876 $hash eq $check_hash;
880 return 0 if $self->_password eq '';
882 $self->_password eq $check_password;
888 =item change_password NEW_PASSWORD
890 Changes the contact's selfservice access password to NEW_PASSWORD. This does
891 not check password policy rules (see C<is_password_allowed>) and will return
892 an error only if editing the record fails for some reason.
894 If NEW_PASSWORD is the same as the existing password, this does nothing.
898 sub change_password {
899 my($self, $new_password) = @_;
901 # do nothing if the password is unchanged
902 return if $self->authenticate_password($new_password);
904 $self->change_password_fields( $new_password );
910 sub change_password_fields {
911 my($self, $new_password) = @_;
913 $self->_password_encoding('bcrypt');
917 my $salt = pack( 'C*', map int(rand(256)), 1..16 );
919 my $hash = bcrypt_hash( { key_nul => 1,
927 join(',', $cost, en_base64($salt), en_base64($hash) )
932 # end of false laziness w/FS/FS/Auth/internal.pm
935 #false laziness w/ClientAPI/MyAccount/reset_passwd
936 use Digest::SHA qw(sha512_hex);
938 use FS::ClientAPI_SessionCache;
939 sub send_reset_email {
940 my( $self, %opt ) = @_;
942 my @contact_email = $self->contact_email or return '';
944 my $reset_session = {
945 'contactnum' => $self->contactnum,
946 'svcnum' => $opt{'svcnum'},
950 my $conf = new FS::Conf;
952 ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours';
954 my $reset_session_id;
956 $reset_session_id = sha512_hex(time(). {}. rand(). $$)
957 } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
960 $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
965 my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
966 $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
968 my $agentnum = $cust_main ? $cust_main->agentnum : '';
969 my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
970 #die "selfservice-password_reset_msgnum unset" unless $msgnum;
971 return "selfservice-password_reset_msgnum unset" unless $msgnum;
972 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
973 return "selfservice-password_reset_msgnum cannot be loaded" unless $msg_template;
975 'to' => join(',', map $_->emailaddress, @contact_email ),
976 'cust_main' => $cust_main,
978 'substitutions' => { 'session_id' => $reset_session_id }
981 if ( $opt{'queue'} ) { #or should queueing just be the default?
983 my $cust_msg = $msg_template->prepare( %msg_template );
984 my $error = $cust_msg->insert;
985 return $error if $error;
986 my $queue = new FS::queue {
987 'job' => 'FS::cust_msg::process_send',
988 'custnum' => $cust_main ? $cust_main->custnum : '',
990 $queue->insert( $cust_msg->custmsgnum );
994 $msg_template->send( %msg_template );
1000 use vars qw( $myaccount_cache );
1001 sub myaccount_cache {
1003 $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
1004 'namespace' => 'FS::ClientAPI::MyAccount',
1008 =item cgi_contact_fields
1010 Returns a list reference containing the set of contact fields used in the web
1011 interface for one-line editing (i.e. excluding contactnum, prospectnum, custnum
1012 and locationnum, as well as password fields, but including fields for
1013 contact_email and contact_phone records.)
1017 sub cgi_contact_fields {
1020 my @contact_fields = qw(
1021 classnum first last title comment emailaddress selfservice_access
1022 invoice_dest message_dest password
1025 push @contact_fields, 'phonetypenum'. $_->phonetypenum
1026 foreach qsearch({table=>'phone_type', order_by=>'weight'});
1032 use FS::upgrade_journal;
1033 sub _upgrade_data { #class method
1034 my ($class, %opts) = @_;
1036 # before anything else, migrate contact.custnum to cust_contact records
1037 unless ( FS::upgrade_journal->is_done('contact_invoice_dest') ) {
1039 local($skip_fuzzyfiles) = 1;
1041 foreach my $contact (qsearch('contact', {})) {
1042 my $error = $contact->replace;
1043 die $error if $error;
1046 FS::upgrade_journal->set_done('contact_invoice_dest');
1050 # always migrate cust_main_invoice records over
1051 local $FS::cust_main::import = 1; # override require_phone and such
1052 my $search = FS::Cursor->new('cust_main_invoice', {});
1054 while (my $cust_main_invoice = $search->fetch) {
1055 my $custnum = $cust_main_invoice->custnum;
1056 my $dest = $cust_main_invoice->dest;
1057 my $cust_main = $cust_main_invoice->cust_main;
1059 if ( $dest =~ /^\d+$/ ) {
1060 my $svc_acct = FS::svc_acct->by_key($dest);
1061 die "custnum $custnum, invoice destination svcnum $svc_acct does not exist\n"
1063 $dest = $svc_acct->email;
1065 push @{ $custnum_dest{$custnum} ||= [] }, $dest;
1067 my $error = $cust_main_invoice->delete;
1069 die "custnum $custnum, cleaning up cust_main_invoice: $error\n";
1073 foreach my $custnum (keys %custnum_dest) {
1074 my $dests = $custnum_dest{$custnum};
1075 my $cust_main = FS::cust_main->by_key($custnum);
1076 my $error = $cust_main->replace( invoicing_list => $dests );
1078 die "custnum $custnum, creating contact: $error\n";
1090 L<FS::Record>, schema.html from the base documentation.