2 use base qw( FS::Record );
5 use vars qw( $skip_fuzzyfiles );
6 use Scalar::Util qw( blessed );
7 use FS::Record qw( qsearchs dbh ); # qw( qsearch qsearchs dbh );
16 FS::contact - Object methods for contact records
22 $record = new FS::contact \%hash;
23 $record = new FS::contact { 'column' => 'value' };
25 $error = $record->insert;
27 $error = $new_record->replace($old_record);
29 $error = $record->delete;
31 $error = $record->check;
35 An FS::contact object represents an example. FS::contact inherits from
36 FS::Record. The following fields are currently supported:
72 =item selfservice_access
78 =item _password_encoding
95 Creates a new example. To add the example to the database, see L<"insert">.
97 Note that this stores the hash reference, not a distinct copy of the hash it
98 points to. You can ask the object for a copy with the I<hash> method.
102 # the new method can be inherited from FS::Record, if a table method is defined
104 sub table { 'contact'; }
108 Adds this record to the database. If there is an error, returns the error,
109 otherwise returns false.
116 local $SIG{INT} = 'IGNORE';
117 local $SIG{QUIT} = 'IGNORE';
118 local $SIG{TERM} = 'IGNORE';
119 local $SIG{TSTP} = 'IGNORE';
120 local $SIG{PIPE} = 'IGNORE';
122 my $oldAutoCommit = $FS::UID::AutoCommit;
123 local $FS::UID::AutoCommit = 0;
126 my $error = $self->SUPER::insert;
128 $dbh->rollback if $oldAutoCommit;
132 foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
133 keys %{ $self->hashref } ) {
134 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
135 my $phonetypenum = $1;
137 my $contact_phone = new FS::contact_phone {
138 'contactnum' => $self->contactnum,
139 'phonetypenum' => $phonetypenum,
140 _parse_phonestring( $self->get($pf) ),
142 $error = $contact_phone->insert;
144 $dbh->rollback if $oldAutoCommit;
149 if ( $self->get('emailaddress') =~ /\S/ ) {
151 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
153 my $contact_email = new FS::contact_email {
154 'contactnum' => $self->contactnum,
155 'emailaddress' => $email,
157 $error = $contact_email->insert;
159 $dbh->rollback if $oldAutoCommit;
167 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
168 #warn " queueing fuzzyfiles update\n"
170 $error = $self->queue_fuzzyfiles_update;
172 $dbh->rollback if $oldAutoCommit;
173 return "updating fuzzy search cache: $error";
177 if ( $self->selfservice_access ) {
178 my $error = $self->send_reset_email( queue=>1 );
180 $dbh->rollback if $oldAutoCommit;
185 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
193 Delete this record from the database.
197 # the delete method can be inherited from FS::Record
202 local $SIG{HUP} = 'IGNORE';
203 local $SIG{INT} = 'IGNORE';
204 local $SIG{QUIT} = 'IGNORE';
205 local $SIG{TERM} = 'IGNORE';
206 local $SIG{TSTP} = 'IGNORE';
207 local $SIG{PIPE} = 'IGNORE';
209 my $oldAutoCommit = $FS::UID::AutoCommit;
210 local $FS::UID::AutoCommit = 0;
213 foreach my $cust_pkg ( $self->cust_pkg ) {
214 $cust_pkg->contactnum('');
215 my $error = $cust_pkg->replace;
217 $dbh->rollback if $oldAutoCommit;
222 foreach my $object ( $self->contact_phone, $self->contact_email ) {
223 my $error = $object->delete;
225 $dbh->rollback if $oldAutoCommit;
230 my $error = $self->SUPER::delete;
232 $dbh->rollback if $oldAutoCommit;
236 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
241 =item replace OLD_RECORD
243 Replaces the OLD_RECORD with this one in the database. If there is an error,
244 returns the error, otherwise returns false.
251 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
253 : $self->replace_old;
255 $self->$_( $self->$_ || $old->$_ ) for qw( _password _password_encoding );
257 local $SIG{INT} = 'IGNORE';
258 local $SIG{QUIT} = 'IGNORE';
259 local $SIG{TERM} = 'IGNORE';
260 local $SIG{TSTP} = 'IGNORE';
261 local $SIG{PIPE} = 'IGNORE';
263 my $oldAutoCommit = $FS::UID::AutoCommit;
264 local $FS::UID::AutoCommit = 0;
267 my $error = $self->SUPER::replace($old);
269 $dbh->rollback if $oldAutoCommit;
273 foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) }
274 keys %{ $self->hashref } ) {
275 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
276 my $phonetypenum = $1;
278 my %cp = ( 'contactnum' => $self->contactnum,
279 'phonetypenum' => $phonetypenum,
281 my $contact_phone = qsearchs('contact_phone', \%cp)
282 || new FS::contact_phone \%cp;
284 my %cpd = _parse_phonestring( $self->get($pf) );
285 $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
287 my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
289 $error = $contact_phone->$method;
291 $dbh->rollback if $oldAutoCommit;
296 if ( defined($self->hashref->{'emailaddress'}) ) {
298 #ineffecient but whatever, how many email addresses can there be?
300 foreach my $contact_email ( $self->contact_email ) {
301 my $error = $contact_email->delete;
303 $dbh->rollback if $oldAutoCommit;
308 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
310 my $contact_email = new FS::contact_email {
311 'contactnum' => $self->contactnum,
312 'emailaddress' => $email,
314 $error = $contact_email->insert;
316 $dbh->rollback if $oldAutoCommit;
324 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
325 #warn " queueing fuzzyfiles update\n"
327 $error = $self->queue_fuzzyfiles_update;
329 $dbh->rollback if $oldAutoCommit;
330 return "updating fuzzy search cache: $error";
334 if ( ( $old->selfservice_access eq '' && $self->selfservice_access
335 && ! $self->_password
340 my $error = $self->send_reset_email( queue=>1 );
342 $dbh->rollback if $oldAutoCommit;
347 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
353 #i probably belong in contact_phone.pm
354 sub _parse_phonestring {
357 my($countrycode, $extension) = ('1', '');
360 if ( $value =~ s/^\s*\+\s*(\d+)// ) {
366 if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
370 ( 'countrycode' => $countrycode,
371 'phonenum' => $value,
372 'extension' => $extension,
376 =item queue_fuzzyfiles_update
378 Used by insert & replace to update the fuzzy search cache
382 use FS::cust_main::Search;
383 sub queue_fuzzyfiles_update {
386 local $SIG{HUP} = 'IGNORE';
387 local $SIG{INT} = 'IGNORE';
388 local $SIG{QUIT} = 'IGNORE';
389 local $SIG{TERM} = 'IGNORE';
390 local $SIG{TSTP} = 'IGNORE';
391 local $SIG{PIPE} = 'IGNORE';
393 my $oldAutoCommit = $FS::UID::AutoCommit;
394 local $FS::UID::AutoCommit = 0;
397 foreach my $field ( 'first', 'last' ) {
398 my $queue = new FS::queue {
399 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
401 my @args = "contact.$field", $self->get($field);
402 my $error = $queue->insert( @args );
404 $dbh->rollback if $oldAutoCommit;
405 return "queueing job (transaction rolled back): $error";
409 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
416 Checks all fields to make sure this is a valid example. If there is
417 an error, returns the error, otherwise returns false. Called by the insert
422 # the check method should currently be supplied - FS::Record contains some
423 # data checking routines
428 if ( $self->selfservice_access eq 'R' ) {
429 $self->selfservice_access('Y');
434 $self->ut_numbern('contactnum')
435 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
436 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
437 || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
438 || $self->ut_foreign_keyn('classnum', 'contact_class', 'classnum')
439 || $self->ut_namen('last')
440 || $self->ut_namen('first')
441 || $self->ut_textn('title')
442 || $self->ut_textn('comment')
443 || $self->ut_enum('selfservice_access', [ '', 'Y' ])
444 || $self->ut_textn('_password')
445 || $self->ut_enum('_password_encoding', [ '', 'bcrypt'])
446 || $self->ut_enum('disabled', [ '', 'Y' ])
448 return $error if $error;
450 return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
451 return "Prospect and customer!" if $self->prospectnum && $self->custnum;
453 return "One of first name, last name, or title must have a value"
454 if ! grep $self->$_(), qw( first last title);
461 my $data = $self->first. ' '. $self->last;
462 $data .= ', '. $self->title
464 $data .= ' ('. $self->comment. ')'
471 $self->first . ' ' . $self->last;
474 sub contact_classname {
476 my $contact_class = $self->contact_class or return '';
477 $contact_class->classname;
480 sub by_selfservice_email {
481 my($class, $email) = @_;
483 my $contact_email = qsearchs({
484 'table' => 'contact_email',
485 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
486 'hashref' => { 'emailaddress' => $email, },
487 'extra_sql' => " AND selfservice_access = 'Y' ".
488 " AND ( disabled IS NULL OR disabled = '' )",
491 $contact_email->contact;
495 #these three functions are very much false laziness w/FS/FS/Auth/internal.pm
496 # and should maybe be libraried in some way for other password needs
498 use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
500 sub authenticate_password {
501 my($self, $check_password) = @_;
503 if ( $self->_password_encoding eq 'bcrypt' ) {
505 my( $cost, $salt, $hash ) = split(',', $self->_password);
507 my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
509 salt => de_base64($salt),
515 $hash eq $check_hash;
519 return 0 if $self->_password eq '';
521 $self->_password eq $check_password;
527 sub change_password {
528 my($self, $new_password) = @_;
530 $self->change_password_fields( $new_password );
536 sub change_password_fields {
537 my($self, $new_password) = @_;
539 $self->_password_encoding('bcrypt');
543 my $salt = pack( 'C*', map int(rand(256)), 1..16 );
545 my $hash = bcrypt_hash( { key_nul => 1,
553 join(',', $cost, en_base64($salt), en_base64($hash) )
558 # end of false laziness w/FS/FS/Auth/internal.pm
561 #false laziness w/ClientAPI/MyAccount/reset_passwd
562 use Digest::SHA qw(sha512_hex);
564 use FS::ClientAPI_SessionCache;
565 sub send_reset_email {
566 my( $self, %opt ) = @_;
568 my @contact_email = $self->contact_email or return '';
570 my $reset_session = {
571 'contactnum' => $self->contactnum,
572 'svcnum' => $opt{'svcnum'},
575 my $timeout = '24 hours'; #?
577 my $reset_session_id;
579 $reset_session_id = sha512_hex(time(). {}. rand(). $$)
580 } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
583 $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
587 my $conf = new FS::Conf;
589 my $cust_main = $self->cust_main
590 or die "no customer"; #reset a password for a prospect contact? someday
592 my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
593 #die "selfservice-password_reset_msgnum unset" unless $msgnum;
594 return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
595 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
597 'to' => join(',', map $_->emailaddress, @contact_email ),
598 'cust_main' => $cust_main,
600 'substitutions' => { 'session_id' => $reset_session_id }
603 if ( $opt{'queue'} ) { #or should queueing just be the default?
605 my $queue = new FS::queue {
606 'job' => 'FS::Misc::process_send_email',
607 'custnum' => $cust_main->custnum,
609 $queue->insert( $msg_template->prepare( %msg_template ) );
613 $msg_template->send( %msg_template );
619 use vars qw( $myaccount_cache );
620 sub myaccount_cache {
622 $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
623 'namespace' => 'FS::ClientAPI::MyAccount',
633 L<FS::Record>, schema.html from the base documentation.