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 =item by_selfservice_email EMAILADDRESS
824 Alternate search constructor (class method). Given an email address, returns
825 the contact for that address. If that contact doesn't have selfservice access,
826 or there isn't one, returns the empty string.
830 sub by_selfservice_email {
831 my($class, $email, $case_insensitive) = @_;
833 my $email_search = "emailaddress = '".$email."'";
834 $email_search = "LOWER(emailaddress) = LOWER('".$email."')" if $case_insensitive;
836 my $contact_email = qsearchs({
837 'table' => 'contact_email',
838 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
841 AND ( contact.disabled IS NULL )
842 AND EXISTS ( SELECT 1 FROM cust_contact
843 WHERE contact.contactnum = cust_contact.contactnum
844 AND cust_contact.selfservice_access = 'Y'
849 $contact_email->contact;
853 =item by_selfservice_email_custnum EMAILADDRESS, CUSTNUM
855 Alternate search constructor (class method). Given an email address and custnum, returns
856 the contact for that address and custnum. If that contact doesn't have selfservice access,
857 or there isn't one, returns the empty string.
861 sub by_selfservice_email_custnum {
862 my($class, $email, $custnum) = @_;
864 my $contact_email = qsearchs({
865 'table' => 'contact_email',
866 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
867 'hashref' => { 'emailaddress' => $email, },
869 AND ( contact.disabled IS NULL )
870 AND EXISTS ( SELECT 1 FROM cust_contact
871 WHERE contact.contactnum = cust_contact.contactnum
872 AND cust_contact.selfservice_access = 'Y'
873 AND cust_contact.custnum = $custnum
878 $contact_email->contact;
882 #these three functions are very much false laziness w/FS/FS/Auth/internal.pm
883 # and should maybe be libraried in some way for other password needs
885 use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
887 sub authenticate_password {
888 my($self, $check_password) = @_;
890 if ( $self->_password_encoding eq 'bcrypt' ) {
892 my( $cost, $salt, $hash ) = split(',', $self->_password);
894 my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
896 salt => de_base64($salt),
902 $hash eq $check_hash;
906 return 0 if $self->_password eq '';
908 $self->_password eq $check_password;
914 =item change_password NEW_PASSWORD
916 Changes the contact's selfservice access password to NEW_PASSWORD. This does
917 not check password policy rules (see C<is_password_allowed>) and will return
918 an error only if editing the record fails for some reason.
920 If NEW_PASSWORD is the same as the existing password, this does nothing.
924 sub change_password {
925 my($self, $new_password) = @_;
927 # do nothing if the password is unchanged
928 return if $self->authenticate_password($new_password);
930 $self->change_password_fields( $new_password );
936 sub change_password_fields {
937 my($self, $new_password) = @_;
939 $self->_password_encoding('bcrypt');
943 my $salt = pack( 'C*', map int(rand(256)), 1..16 );
945 my $hash = bcrypt_hash( { key_nul => 1,
953 join(',', $cost, en_base64($salt), en_base64($hash) )
958 # end of false laziness w/FS/FS/Auth/internal.pm
961 #false laziness w/ClientAPI/MyAccount/reset_passwd
962 use Digest::SHA qw(sha512_hex);
964 use FS::ClientAPI_SessionCache;
965 sub send_reset_email {
966 my( $self, %opt ) = @_;
968 my @contact_email = $self->contact_email or return '';
970 my $reset_session = {
971 'contactnum' => $self->contactnum,
972 'svcnum' => $opt{'svcnum'},
976 my $conf = new FS::Conf;
978 ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours';
980 my $reset_session_id;
982 $reset_session_id = sha512_hex(time(). {}. rand(). $$)
983 } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
986 $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
991 my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
992 $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
994 my $agentnum = $cust_main ? $cust_main->agentnum : '';
995 my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
996 #die "selfservice-password_reset_msgnum unset" unless $msgnum;
997 return "selfservice-password_reset_msgnum unset" unless $msgnum;
998 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
999 return "selfservice-password_reset_msgnum cannot be loaded" unless $msg_template;
1000 my %msg_template = (
1001 'to' => join(',', map $_->emailaddress, @contact_email ),
1002 'cust_main' => $cust_main,
1004 'substitutions' => { 'session_id' => $reset_session_id }
1007 if ( $opt{'queue'} ) { #or should queueing just be the default?
1009 my $cust_msg = $msg_template->prepare( %msg_template );
1010 my $error = $cust_msg->insert;
1011 return $error if $error;
1012 my $queue = new FS::queue {
1013 'job' => 'FS::cust_msg::process_send',
1014 'custnum' => $cust_main ? $cust_main->custnum : '',
1016 $queue->insert( $cust_msg->custmsgnum );
1020 $msg_template->send( %msg_template );
1026 use vars qw( $myaccount_cache );
1027 sub myaccount_cache {
1029 $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
1030 'namespace' => 'FS::ClientAPI::MyAccount',
1034 =item cgi_contact_fields
1036 Returns a list reference containing the set of contact fields used in the web
1037 interface for one-line editing (i.e. excluding contactnum, prospectnum, custnum
1038 and locationnum, as well as password fields, but including fields for
1039 contact_email and contact_phone records.)
1043 sub cgi_contact_fields {
1046 my @contact_fields = qw(
1047 classnum first last title comment emailaddress selfservice_access
1048 invoice_dest message_dest password
1051 push @contact_fields, 'phonetypenum'. $_->phonetypenum
1052 foreach qsearch({table=>'phone_type', order_by=>'weight'});
1058 use FS::upgrade_journal;
1059 sub _upgrade_data { #class method
1060 my ($class, %opts) = @_;
1062 # before anything else, migrate contact.custnum to cust_contact records
1063 unless ( FS::upgrade_journal->is_done('contact_invoice_dest') ) {
1065 local($skip_fuzzyfiles) = 1;
1067 foreach my $contact (qsearch('contact', {})) {
1068 my $error = $contact->replace;
1069 die $error if $error;
1072 FS::upgrade_journal->set_done('contact_invoice_dest');
1076 # always migrate cust_main_invoice records over
1077 local $FS::cust_main::import = 1; # override require_phone and such
1078 my $search = FS::Cursor->new('cust_main_invoice', {});
1080 while (my $cust_main_invoice = $search->fetch) {
1081 my $custnum = $cust_main_invoice->custnum;
1082 my $dest = $cust_main_invoice->dest;
1083 my $cust_main = $cust_main_invoice->cust_main;
1085 if ( $dest =~ /^\d+$/ ) {
1086 my $svc_acct = FS::svc_acct->by_key($dest);
1087 die "custnum $custnum, invoice destination svcnum $svc_acct does not exist\n"
1089 $dest = $svc_acct->email;
1091 push @{ $custnum_dest{$custnum} ||= [] }, $dest;
1093 my $error = $cust_main_invoice->delete;
1095 die "custnum $custnum, cleaning up cust_main_invoice: $error\n";
1099 foreach my $custnum (keys %custnum_dest) {
1100 my $dests = $custnum_dest{$custnum};
1101 my $cust_main = FS::cust_main->by_key($custnum);
1102 my $error = $cust_main->replace( invoicing_list => $dests );
1104 die "custnum $custnum, creating contact: $error\n";
1116 L<FS::Record>, schema.html from the base documentation.