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, $case_insensitive) = @_;
839 my $email_search = "emailaddress = '".$email."'";
840 $email_search = "LOWER(emailaddress) = LOWER('".$email."')" if $case_insensitive;
842 my $contact_email = qsearchs({
843 'table' => 'contact_email',
844 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
847 AND ( contact.disabled IS NULL )
848 AND EXISTS ( SELECT 1 FROM cust_contact
849 WHERE contact.contactnum = cust_contact.contactnum
850 AND cust_contact.selfservice_access = 'Y'
855 $contact_email->contact;
859 =item by_selfservice_email_custnum EMAILADDRESS, CUSTNUM
861 Alternate search constructor (class method). Given an email address and custnum, returns
862 the contact for that address and custnum. If that contact doesn't have selfservice access,
863 or there isn't one, returns the empty string.
867 sub by_selfservice_email_custnum {
868 my($class, $email, $custnum) = @_;
870 my $contact_email = qsearchs({
871 'table' => 'contact_email',
872 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
873 'hashref' => { 'emailaddress' => $email, },
875 AND ( contact.disabled IS NULL )
876 AND EXISTS ( SELECT 1 FROM cust_contact
877 WHERE contact.contactnum = cust_contact.contactnum
878 AND cust_contact.selfservice_access = 'Y'
879 AND cust_contact.custnum = $custnum
884 $contact_email->contact;
888 #these three functions are very much false laziness w/FS/FS/Auth/internal.pm
889 # and should maybe be libraried in some way for other password needs
891 use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
893 sub authenticate_password {
894 my($self, $check_password) = @_;
896 if ( $self->_password_encoding eq 'bcrypt' ) {
898 my( $cost, $salt, $hash ) = split(',', $self->_password);
900 my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
902 salt => de_base64($salt),
908 $hash eq $check_hash;
912 return 0 if $self->_password eq '';
914 $self->_password eq $check_password;
920 =item change_password NEW_PASSWORD
922 Changes the contact's selfservice access password to NEW_PASSWORD. This does
923 not check password policy rules (see C<is_password_allowed>) and will return
924 an error only if editing the record fails for some reason.
926 If NEW_PASSWORD is the same as the existing password, this does nothing.
930 sub change_password {
931 my($self, $new_password) = @_;
933 # do nothing if the password is unchanged
934 return if $self->authenticate_password($new_password);
936 $self->change_password_fields( $new_password );
942 sub change_password_fields {
943 my($self, $new_password) = @_;
945 $self->_password_encoding('bcrypt');
949 my $salt = pack( 'C*', map int(rand(256)), 1..16 );
951 my $hash = bcrypt_hash( { key_nul => 1,
959 join(',', $cost, en_base64($salt), en_base64($hash) )
964 # end of false laziness w/FS/FS/Auth/internal.pm
967 #false laziness w/ClientAPI/MyAccount/reset_passwd
968 use Digest::SHA qw(sha512_hex);
970 use FS::ClientAPI_SessionCache;
971 sub send_reset_email {
972 my( $self, %opt ) = @_;
974 my @contact_email = $self->contact_email or return '';
976 my $reset_session = {
977 'contactnum' => $self->contactnum,
978 'svcnum' => $opt{'svcnum'},
982 my $conf = new FS::Conf;
984 ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours';
986 my $reset_session_id;
988 $reset_session_id = sha512_hex(time(). {}. rand(). $$)
989 } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
992 $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
997 my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
998 $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
1000 my $agentnum = $cust_main ? $cust_main->agentnum : '';
1001 my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
1002 #die "selfservice-password_reset_msgnum unset" unless $msgnum;
1003 return "selfservice-password_reset_msgnum unset" unless $msgnum;
1004 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
1005 return "selfservice-password_reset_msgnum cannot be loaded" unless $msg_template;
1006 my %msg_template = (
1007 'to' => join(',', map $_->emailaddress, @contact_email ),
1008 'cust_main' => $cust_main,
1010 'substitutions' => { 'session_id' => $reset_session_id }
1013 if ( $opt{'queue'} ) { #or should queueing just be the default?
1015 my $cust_msg = $msg_template->prepare( %msg_template );
1016 my $error = $cust_msg->insert;
1017 return $error if $error;
1018 my $queue = new FS::queue {
1019 'job' => 'FS::cust_msg::process_send',
1020 'custnum' => $cust_main ? $cust_main->custnum : '',
1022 $queue->insert( $cust_msg->custmsgnum );
1026 $msg_template->send( %msg_template );
1032 use vars qw( $myaccount_cache );
1033 sub myaccount_cache {
1035 $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
1036 'namespace' => 'FS::ClientAPI::MyAccount',
1040 =item cgi_contact_fields
1042 Returns a list reference containing the set of contact fields used in the web
1043 interface for one-line editing (i.e. excluding contactnum, prospectnum, custnum
1044 and locationnum, as well as password fields, but including fields for
1045 contact_email and contact_phone records.)
1049 sub cgi_contact_fields {
1052 my @contact_fields = qw(
1053 classnum first last title comment emailaddress selfservice_access
1054 invoice_dest message_dest password
1057 push @contact_fields, 'phonetypenum'. $_->phonetypenum
1058 foreach qsearch({table=>'phone_type', order_by=>'weight'});
1064 use FS::upgrade_journal;
1065 sub _upgrade_data { #class method
1066 my ($class, %opts) = @_;
1068 # before anything else, migrate contact.custnum to cust_contact records
1069 unless ( FS::upgrade_journal->is_done('contact_invoice_dest') ) {
1071 local($skip_fuzzyfiles) = 1;
1073 foreach my $contact (qsearch('contact', {})) {
1074 my $error = $contact->replace;
1075 die $error if $error;
1078 FS::upgrade_journal->set_done('contact_invoice_dest');
1082 # always migrate cust_main_invoice records over
1083 local $FS::cust_main::import = 1; # override require_phone and such
1084 my $search = FS::Cursor->new('cust_main_invoice', {});
1086 while (my $cust_main_invoice = $search->fetch) {
1087 my $custnum = $cust_main_invoice->custnum;
1088 my $dest = $cust_main_invoice->dest;
1089 my $cust_main = $cust_main_invoice->cust_main;
1091 if ( $dest =~ /^\d+$/ ) {
1092 my $svc_acct = FS::svc_acct->by_key($dest);
1093 die "custnum $custnum, invoice destination svcnum $svc_acct does not exist\n"
1095 $dest = $svc_acct->email;
1097 push @{ $custnum_dest{$custnum} ||= [] }, $dest;
1099 my $error = $cust_main_invoice->delete;
1101 die "custnum $custnum, cleaning up cust_main_invoice: $error\n";
1105 foreach my $custnum (keys %custnum_dest) {
1106 my $dests = $custnum_dest{$custnum};
1107 my $cust_main = FS::cust_main->by_key($custnum);
1108 my $error = $cust_main->replace( invoicing_list => $dests );
1110 die "custnum $custnum, creating contact: $error\n";
1122 L<FS::Record>, schema.html from the base documentation.