X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcontact.pm;h=13f07a2838c3559cecefa78d5f89fe0aada14607;hp=89bfb745b3c4cb48dac5bcd403bdea162b54cdcd;hb=5ef60116554ff9dbd01bbb4daa4e1ba098905311;hpb=ec7e8155fce544f19f2b6734476ed6db8c200aa9 diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm index 89bfb745b..13f07a283 100644 --- a/FS/FS/contact.pm +++ b/FS/FS/contact.pm @@ -1,13 +1,16 @@ package FS::contact; -use base qw( FS::Record ); +use base qw( FS::Password_Mixin + FS::Record ); use strict; use vars qw( $skip_fuzzyfiles ); use Carp; use Scalar::Util qw( blessed ); use FS::Record qw( qsearch qsearchs dbh ); +use FS::Cursor; use FS::contact_phone; use FS::contact_email; +use FS::contact::Import; use FS::queue; use FS::phone_type; #for cgi_contact_fields use FS::cust_contact; @@ -88,7 +91,6 @@ empty or bcrypt disabled - =back =head1 METHODS @@ -111,6 +113,26 @@ sub table { 'contact'; } Adds this record to the database. If there is an error, returns the error, otherwise returns false. +If the object has an C field, L records +will be created for each (comma-separated) email address in that field. If +any of these coincide with an existing email address, this contact will be +merged with the contact with that address. + +Then, if the object has any fields named C an +L record will be created for each of them. Those fields +should contain phone numbers of the appropriate types (where N is the key of +an L record identifying the type of number: daytime, night, +etc.). + +After inserting the record, if the object has a 'custnum' or 'prospectnum' +field, an L or L record will be +created to link the contact to the customer. The following fields will also +be included in that record, if they are set on the object: +- classnum +- comment +- selfservice_access +- invoice_dest + =cut sub insert { @@ -133,69 +155,75 @@ sub insert { $self->custnum(''); my %link_hash = (); - for (qw( classnum comment selfservice_access )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest)) { $link_hash{$_} = $self->get($_); $self->$_(''); } - #look for an existing contact with this email address + + ## check for an existing contact with this email address other than current customer + ## if found, just add that contact to cust_contact with link_hash credentials + ## as email can not be tied to two contacts. + my $no_new_contact; my $existing_contact = ''; + my @contact_emails = (); + my %contact_nums = (); + $contact_nums{$self->contactnum} = '1' if $self->contactnum; + if ( $self->get('emailaddress') =~ /\S/ ) { - - my %existing_contact = (); foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) { - my $contact_email = qsearchs('contact_email', { emailaddress=>$email } ) - or next; + my $contact_email = qsearchs('contact_email', { emailaddress=>$email } ); + unless ($contact_email) { push @contact_emails, $email; next; } my $contact = $contact_email->contact; - $existing_contact{ $contact->contactnum } = $contact; - - } + if ($contact->contactnum eq $self->contactnum) { + push @contact_emails, $email; + } + else { + $contact_nums{$contact->contactnum} = '1'; + } - if ( scalar( keys %existing_contact ) > 1 ) { - $dbh->rollback if $oldAutoCommit; - return 'Multiple email addresses specified '. - ' that already belong to separate contacts'; - } elsif ( scalar( keys %existing_contact ) ) { - ($existing_contact) = values %existing_contact; } - } + my $emails = join(' , ', @contact_emails); + $self->emailaddress($emails); - if ( $existing_contact ) { + $no_new_contact = '1' unless $self->emailaddress; - $self->$_($existing_contact->$_()) - for qw( contactnum _password _password_encoding ); - $self->SUPER::replace($existing_contact); + } - } else { - - my $error = $self->SUPER::insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } + my $error; + $error = $self->SUPER::insert unless $no_new_contact; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; } my $cust_contact = ''; + # if $self->custnum was set, then the customer-specific properties + # (custnum, classnum, invoice_dest, selfservice_access, comment) are in + # pseudo-fields, and are now in %link_hash. otherwise, ignore all those + # fields. if ( $custnum ) { - my %hash = ( 'contactnum' => $self->contactnum, - 'custnum' => $custnum, - ); - $cust_contact = qsearchs('cust_contact', \%hash ) - || new FS::cust_contact { %hash, %link_hash }; - my $error = $cust_contact->custcontactnum ? $cust_contact->replace + foreach my $contactnum (keys %contact_nums) { + my %hash = ( 'contactnum' => $contactnum, + 'custnum' => $custnum, + ); + $cust_contact = qsearchs('cust_contact', \%hash ) + || new FS::cust_contact { %hash, %link_hash }; + my $error = $cust_contact->custcontactnum ? $cust_contact->replace : $cust_contact->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } } - if ( $prospectnum ) { + if ( $prospectnum && !$no_new_contact) { my %hash = ( 'contactnum' => $self->contactnum, 'prospectnum' => $prospectnum, ); @@ -210,6 +238,7 @@ sub insert { } } + unless ($no_new_contact) { foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ } keys %{ $self->hashref } ) { $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)"; @@ -228,6 +257,7 @@ sub insert { return $error; } } + } if ( $self->get('emailaddress') =~ /\S/ ) { @@ -259,7 +289,10 @@ sub insert { } if ( $link_hash{'selfservice_access'} eq 'R' - or ( $link_hash{'selfservice_access'} && $cust_contact ) + or ( $link_hash{'selfservice_access'} + && $cust_contact + && ! length($self->_password) + ) ) { my $error = $self->send_reset_email( queue=>1 ); @@ -269,6 +302,15 @@ sub insert { } } + if ( $self->get('password') ) { + my $error = $self->is_password_allowed($self->get('password')) + || $self->change_password($self->get('password')); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -309,6 +351,8 @@ sub delete { } } + # if $self->custnum was set, then we're removing the contact from this + # customer. if ( $self->custnum ) { my $cust_contact = qsearchs('cust_contact', { 'contactnum' => $self->contactnum, @@ -350,7 +394,8 @@ sub delete { } } - my $error = $self->SUPER::delete; + my $error = $self->delete_password_history + || $self->SUPER::delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -391,36 +436,83 @@ sub replace { my $prospectnum = $self->prospectnum; $self->prospectnum(''); my $custnum = $self->custnum; - $self->custnum(''); + $self->custnum(''); $old->custnum(''); # remove because now stored cust_contact my %link_hash = (); - for (qw( classnum comment selfservice_access )) { + for (qw( classnum comment selfservice_access invoice_dest message_dest )) { $link_hash{$_} = $self->get($_); + $old->$_(''); ##remove values from old record, causes problem with history $self->$_(''); } + ## check for an existing contact with this email address other than current customer + ## if found, just add that contact to cust_contact with link_hash credentials + ## as email can not be tied to two contacts. + my @contact_emails = (); + my %contact_nums = (); + $contact_nums{$self->contactnum} = '1' if $self->contactnum; + if ( $self->get('emailaddress') =~ /\S/ ) { + + foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) { + + my $contact_email = qsearchs('contact_email', { emailaddress=>$email } ); + unless ($contact_email) { push @contact_emails, $email; next; } + + my $contact = $contact_email->contact; + if ($contact->contactnum eq $self->contactnum) { + push @contact_emails, $email; + } + else { + $contact_nums{$contact->contactnum} = '1'; + } + + } + + ## were all emails duplicates? if so reset original emails + if (scalar @contact_emails < 1 && scalar (keys %contact_nums) > 1) { + foreach (qsearch('contact_email', {'contactnum' => $self->contactnum})) { + push @contact_emails, $_->emailaddress; + } + } + + my $emails = join(' , ', @contact_emails); + $self->emailaddress($emails); + + } + my $error = $self->SUPER::replace($old); + if ( $old->_password ne $self->_password ) { + $error ||= $self->insert_password_history; + } if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; } my $cust_contact = ''; + # if $self->custnum was set, then the customer-specific properties + # (custnum, classnum, invoice_dest, selfservice_access, comment) are in + # pseudo-fields, and are now in %link_hash. otherwise, ignore all those + # fields. if ( $custnum ) { - my %hash = ( 'contactnum' => $self->contactnum, - 'custnum' => $custnum, - ); - my $error; - if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) { - $cust_contact->$_($link_hash{$_}) for keys %link_hash; - $error = $cust_contact->replace; - } else { - $cust_contact = new FS::cust_contact { %hash, %link_hash }; - $error = $cust_contact->insert; - } - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; + + foreach my $contactnum (keys %contact_nums) { + + my %hash = ( 'contactnum' => $contactnum, + 'custnum' => $custnum, + ); + my $error; + if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) { + $cust_contact->$_($link_hash{$_}) for keys %link_hash; + $error = $cust_contact->replace; + } else { + $cust_contact = new FS::cust_contact { %hash, %link_hash }; + $error = $cust_contact->insert; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } } @@ -483,30 +575,38 @@ sub replace { if ( defined($self->hashref->{'emailaddress'}) ) { - #ineffecient but whatever, how many email addresses can there be? - + my %contact_emails = (); foreach my $contact_email ( $self->contact_email ) { - my $error = $contact_email->delete; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } + $contact_emails{$contact_email->emailaddress} = '1'; } foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) { - my $contact_email = new FS::contact_email { - 'contactnum' => $self->contactnum, - 'emailaddress' => $email, - }; - $error = $contact_email->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; + unless ($contact_emails{$email}) { + my $contact_email = new FS::contact_email { + 'contactnum' => $self->contactnum, + 'emailaddress' => $email, + }; + $error = $contact_email->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } } + else { delete($contact_emails{$email}); } } + foreach my $contact_email ( $self->contact_email ) { + if ($contact_emails{$contact_email->emailaddress}) { + my $error = $contact_email->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + } unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) { @@ -535,6 +635,15 @@ sub replace { } } + if ( $self->get('password') ) { + my $error = $self->is_password_allowed($self->get('password')) + || $self->change_password($self->get('password')); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -621,7 +730,7 @@ and replace methods. sub check { my $self = shift; - if ( $self->selfservice_access eq 'R' ) { + if ( $self->selfservice_access eq 'R' || $self->selfservice_access eq 'P' ) { $self->selfservice_access('Y'); $self->_resend('Y'); } @@ -711,9 +820,9 @@ sub firstlast { =item by_selfservice_email EMAILADDRESS -Alternate search constructor (class method). Given an email address, -returns the contact for that address, or the empty string if no contact -has that email address. +Alternate search constructor (class method). Given an email address, returns +the contact for that address. If that contact doesn't have selfservice access, +or there isn't one, returns the empty string. =cut @@ -724,7 +833,13 @@ sub by_selfservice_email { 'table' => 'contact_email', 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ', 'hashref' => { 'emailaddress' => $email, }, - 'extra_sql' => " AND ( disabled IS NULL OR disabled = '' )", + 'extra_sql' => " + AND ( contact.disabled IS NULL ) + AND EXISTS ( SELECT 1 FROM cust_contact + WHERE contact.contactnum = cust_contact.contactnum + AND cust_contact.selfservice_access = 'Y' + ) + ", }) or return ''; $contact_email->contact; @@ -753,7 +868,7 @@ sub authenticate_password { $hash eq $check_hash; - } else { + } else { return 0 if $self->_password eq ''; @@ -763,9 +878,22 @@ sub authenticate_password { } +=item change_password NEW_PASSWORD + +Changes the contact's selfservice access password to NEW_PASSWORD. This does +not check password policy rules (see C) and will return +an error only if editing the record fails for some reason. + +If NEW_PASSWORD is the same as the existing password, this does nothing. + +=cut + sub change_password { my($self, $new_password) = @_; + # do nothing if the password is unchanged + return if $self->authenticate_password($new_password); + $self->change_password_fields( $new_password ); $self->replace; @@ -811,7 +939,10 @@ sub send_reset_email { 'svcnum' => $opt{'svcnum'}, }; - my $timeout = '24 hours'; #? + + my $conf = new FS::Conf; + my $timeout = + ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours'; my $reset_session_id; do { @@ -823,8 +954,6 @@ sub send_reset_email { #email it - my $conf = new FS::Conf; - my $cust_main = ''; my @cust_contact = grep $_->selfservice_access, $self->cust_contact; $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1; @@ -832,8 +961,9 @@ sub send_reset_email { my $agentnum = $cust_main ? $cust_main->agentnum : ''; my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum); #die "selfservice-password_reset_msgnum unset" unless $msgnum; - return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum; + return "selfservice-password_reset_msgnum unset" unless $msgnum; my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } ); + return "selfservice-password_reset_msgnum cannot be loaded" unless $msg_template; my %msg_template = ( 'to' => join(',', map $_->emailaddress, @contact_email ), 'cust_main' => $cust_main, @@ -843,11 +973,14 @@ sub send_reset_email { if ( $opt{'queue'} ) { #or should queueing just be the default? + my $cust_msg = $msg_template->prepare( %msg_template ); + my $error = $cust_msg->insert; + return $error if $error; my $queue = new FS::queue { - 'job' => 'FS::Misc::process_send_email', + 'job' => 'FS::cust_msg::process_send', 'custnum' => $cust_main ? $cust_main->custnum : '', }; - $queue->insert( $msg_template->prepare( %msg_template ) ); + $queue->insert( $cust_msg->custmsgnum ); } else { @@ -879,6 +1012,7 @@ sub cgi_contact_fields { my @contact_fields = qw( classnum first last title comment emailaddress selfservice_access + invoice_dest message_dest password ); push @contact_fields, 'phonetypenum'. $_->phonetypenum @@ -892,14 +1026,50 @@ use FS::upgrade_journal; sub _upgrade_data { #class method my ($class, %opts) = @_; - unless ( FS::upgrade_journal->is_done('contact__DUPEMAIL') ) { + # before anything else, migrate contact.custnum to cust_contact records + unless ( FS::upgrade_journal->is_done('contact_invoice_dest') ) { + + local($skip_fuzzyfiles) = 1; foreach my $contact (qsearch('contact', {})) { my $error = $contact->replace; die $error if $error; } - FS::upgrade_journal->set_done('contact__DUPEMAIL'); + FS::upgrade_journal->set_done('contact_invoice_dest'); + } + + + # always migrate cust_main_invoice records over + local $FS::cust_main::import = 1; # override require_phone and such + my $search = FS::Cursor->new('cust_main_invoice', {}); + my %custnum_dest; + while (my $cust_main_invoice = $search->fetch) { + my $custnum = $cust_main_invoice->custnum; + my $dest = $cust_main_invoice->dest; + my $cust_main = $cust_main_invoice->cust_main; + + if ( $dest =~ /^\d+$/ ) { + my $svc_acct = FS::svc_acct->by_key($dest); + die "custnum $custnum, invoice destination svcnum $svc_acct does not exist\n" + if !$svc_acct; + $dest = $svc_acct->email; + } + push @{ $custnum_dest{$custnum} ||= [] }, $dest; + + my $error = $cust_main_invoice->delete; + if ( $error ) { + die "custnum $custnum, cleaning up cust_main_invoice: $error\n"; + } + } + + foreach my $custnum (keys %custnum_dest) { + my $dests = $custnum_dest{$custnum}; + my $cust_main = FS::cust_main->by_key($custnum); + my $error = $cust_main->replace( invoicing_list => $dests ); + if ( $error ) { + die "custnum $custnum, creating contact: $error\n"; + } } } @@ -915,4 +1085,3 @@ L, schema.html from the base documentation. =cut 1; -