self-service access for contacts, RT#25533
[freeside.git] / FS / FS / contact.pm
1 package FS::contact;
2 use base qw( FS::Record );
3
4 use strict;
5 use FS::Record qw( qsearchs dbh ); # qw( qsearch qsearchs dbh );
6
7 =head1 NAME
8
9 FS::contact - Object methods for contact records
10
11 =head1 SYNOPSIS
12
13   use FS::contact;
14
15   $record = new FS::contact \%hash;
16   $record = new FS::contact { 'column' => 'value' };
17
18   $error = $record->insert;
19
20   $error = $new_record->replace($old_record);
21
22   $error = $record->delete;
23
24   $error = $record->check;
25
26 =head1 DESCRIPTION
27
28 An FS::contact object represents an example.  FS::contact inherits from
29 FS::Record.  The following fields are currently supported:
30
31 =over 4
32
33 =item contactnum
34
35 primary key
36
37 =item prospectnum
38
39 prospectnum
40
41 =item custnum
42
43 custnum
44
45 =item locationnum
46
47 locationnum
48
49 =item last
50
51 last
52
53 =item first
54
55 first
56
57 =item title
58
59 title
60
61 =item comment
62
63 comment
64
65 =item selfservice_access
66
67 empty or Y
68
69 =item _password
70
71 =item _password_encoding
72
73 empty or bcrypt
74
75 =item disabled
76
77 disabled
78
79
80 =back
81
82 =head1 METHODS
83
84 =over 4
85
86 =item new HASHREF
87
88 Creates a new example.  To add the example to the database, see L<"insert">.
89
90 Note that this stores the hash reference, not a distinct copy of the hash it
91 points to.  You can ask the object for a copy with the I<hash> method.
92
93 =cut
94
95 # the new method can be inherited from FS::Record, if a table method is defined
96
97 sub table { 'contact'; }
98
99 =item insert
100
101 Adds this record to the database.  If there is an error, returns the error,
102 otherwise returns false.
103
104 =cut
105
106 sub insert {
107   my $self = shift;
108
109   local $SIG{INT} = 'IGNORE';
110   local $SIG{QUIT} = 'IGNORE';
111   local $SIG{TERM} = 'IGNORE';
112   local $SIG{TSTP} = 'IGNORE';
113   local $SIG{PIPE} = 'IGNORE';
114
115   my $oldAutoCommit = $FS::UID::AutoCommit;
116   local $FS::UID::AutoCommit = 0;
117   my $dbh = dbh;
118
119   my $error = $self->SUPER::insert;
120   if ( $error ) {
121     $dbh->rollback if $oldAutoCommit;
122     return $error;
123   }
124
125   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
126                         keys %{ $self->hashref } ) {
127     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
128     my $phonetypenum = $1;
129
130     my $contact_phone = new FS::contact_phone {
131       'contactnum' => $self->contactnum,
132       'phonetypenum' => $phonetypenum,
133       _parse_phonestring( $self->get($pf) ),
134     };
135     $error = $contact_phone->insert;
136     if ( $error ) {
137       $dbh->rollback if $oldAutoCommit;
138       return $error;
139     }
140   }
141
142   if ( $self->get('emailaddress') =~ /\S/ ) {
143
144     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
145  
146       my $contact_email = new FS::contact_email {
147         'contactnum'   => $self->contactnum,
148         'emailaddress' => $email,
149       };
150       $error = $contact_email->insert;
151       if ( $error ) {
152         $dbh->rollback if $oldAutoCommit;
153         return $error;
154       }
155
156     }
157
158   }
159
160   #unless ( $import || $skip_fuzzyfiles ) {
161     #warn "  queueing fuzzyfiles update\n"
162     #  if $DEBUG > 1;
163     $error = $self->queue_fuzzyfiles_update;
164     if ( $error ) {
165       $dbh->rollback if $oldAutoCommit;
166       return "updating fuzzy search cache: $error";
167     }
168   #}
169
170   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
171
172   '';
173
174 }
175
176 =item delete
177
178 Delete this record from the database.
179
180 =cut
181
182 # the delete method can be inherited from FS::Record
183
184 sub delete {
185   my $self = shift;
186
187   local $SIG{HUP} = 'IGNORE';
188   local $SIG{INT} = 'IGNORE';
189   local $SIG{QUIT} = 'IGNORE';
190   local $SIG{TERM} = 'IGNORE';
191   local $SIG{TSTP} = 'IGNORE';
192   local $SIG{PIPE} = 'IGNORE';
193
194   my $oldAutoCommit = $FS::UID::AutoCommit;
195   local $FS::UID::AutoCommit = 0;
196   my $dbh = dbh;
197
198   foreach my $object ( $self->contact_phone, $self->contact_email ) {
199     my $error = $object->delete;
200     if ( $error ) {
201       $dbh->rollback if $oldAutoCommit;
202       return $error;
203     }
204   }
205
206   my $error = $self->SUPER::delete;
207   if ( $error ) {
208     $dbh->rollback if $oldAutoCommit;
209     return $error;
210   }
211
212   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
213   '';
214
215 }
216
217 =item replace OLD_RECORD
218
219 Replaces the OLD_RECORD with this one in the database.  If there is an error,
220 returns the error, otherwise returns false.
221
222 =cut
223
224 sub replace {
225   my $self = shift;
226
227   local $SIG{INT} = 'IGNORE';
228   local $SIG{QUIT} = 'IGNORE';
229   local $SIG{TERM} = 'IGNORE';
230   local $SIG{TSTP} = 'IGNORE';
231   local $SIG{PIPE} = 'IGNORE';
232
233   my $oldAutoCommit = $FS::UID::AutoCommit;
234   local $FS::UID::AutoCommit = 0;
235   my $dbh = dbh;
236
237   my $error = $self->SUPER::replace(@_);
238   if ( $error ) {
239     $dbh->rollback if $oldAutoCommit;
240     return $error;
241   }
242
243   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) }
244                         keys %{ $self->hashref } ) {
245     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
246     my $phonetypenum = $1;
247
248     my %cp = ( 'contactnum'   => $self->contactnum,
249                'phonetypenum' => $phonetypenum,
250              );
251     my $contact_phone = qsearchs('contact_phone', \%cp)
252                         || new FS::contact_phone   \%cp;
253
254     my %cpd = _parse_phonestring( $self->get($pf) );
255     $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
256
257     my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
258
259     $error = $contact_phone->$method;
260     if ( $error ) {
261       $dbh->rollback if $oldAutoCommit;
262       return $error;
263     }
264   }
265
266   if ( defined($self->get('emailaddress')) ) {
267
268     #ineffecient but whatever, how many email addresses can there be?
269
270     foreach my $contact_email ( $self->contact_email ) {
271       my $error = $contact_email->delete;
272       if ( $error ) {
273         $dbh->rollback if $oldAutoCommit;
274         return $error;
275       }
276     }
277
278     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
279  
280       my $contact_email = new FS::contact_email {
281         'contactnum'   => $self->contactnum,
282         'emailaddress' => $email,
283       };
284       $error = $contact_email->insert;
285       if ( $error ) {
286         $dbh->rollback if $oldAutoCommit;
287         return $error;
288       }
289
290     }
291
292   }
293
294   #unless ( $import || $skip_fuzzyfiles ) {
295     #warn "  queueing fuzzyfiles update\n"
296     #  if $DEBUG > 1;
297     $error = $self->queue_fuzzyfiles_update;
298     if ( $error ) {
299       $dbh->rollback if $oldAutoCommit;
300       return "updating fuzzy search cache: $error";
301     }
302   #}
303
304   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
305
306   '';
307
308 }
309
310 #i probably belong in contact_phone.pm
311 sub _parse_phonestring {
312   my $value = shift;
313
314   my($countrycode, $extension) = ('1', '');
315
316   #countrycode
317   if ( $value =~ s/^\s*\+\s*(\d+)// ) {
318     $countrycode = $1;
319   } else {
320     $value =~ s/^\s*1//;
321   }
322   #extension
323   if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
324      $extension = $2;
325   }
326
327   ( 'countrycode' => $countrycode,
328     'phonenum'    => $value,
329     'extension'   => $extension,
330   );
331 }
332
333 =item queue_fuzzyfiles_update
334
335 Used by insert & replace to update the fuzzy search cache
336
337 =cut
338
339 use FS::cust_main::Search;
340 sub queue_fuzzyfiles_update {
341   my $self = shift;
342
343   local $SIG{HUP} = 'IGNORE';
344   local $SIG{INT} = 'IGNORE';
345   local $SIG{QUIT} = 'IGNORE';
346   local $SIG{TERM} = 'IGNORE';
347   local $SIG{TSTP} = 'IGNORE';
348   local $SIG{PIPE} = 'IGNORE';
349
350   my $oldAutoCommit = $FS::UID::AutoCommit;
351   local $FS::UID::AutoCommit = 0;
352   my $dbh = dbh;
353
354   foreach my $field ( 'first', 'last' ) {
355     my $queue = new FS::queue { 
356       'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
357     };
358     my @args = "contact.$field", $self->get($field);
359     my $error = $queue->insert( @args );
360     if ( $error ) {
361       $dbh->rollback if $oldAutoCommit;
362       return "queueing job (transaction rolled back): $error";
363     }
364   }
365
366   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
367   '';
368
369 }
370
371 =item check
372
373 Checks all fields to make sure this is a valid example.  If there is
374 an error, returns the error, otherwise returns false.  Called by the insert
375 and replace methods.
376
377 =cut
378
379 # the check method should currently be supplied - FS::Record contains some
380 # data checking routines
381
382 sub check {
383   my $self = shift;
384
385   my $error = 
386     $self->ut_numbern('contactnum')
387     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
388     || $self->ut_foreign_keyn('custnum',     'cust_main',     'custnum')
389     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
390     || $self->ut_foreign_keyn('classnum',    'contact_class', 'classnum')
391     || $self->ut_namen('last')
392     || $self->ut_namen('first')
393     || $self->ut_textn('title')
394     || $self->ut_textn('comment')
395     || $self->ut_enum('selfservice_access', [ '', 'Y' ])
396     || $self->ut_textn('_password')
397     || $self->ut_enum('_password_encoding', [ '', 'bcrypt'])
398     || $self->ut_enum('disabled', [ '', 'Y' ])
399   ;
400   return $error if $error;
401
402   return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
403   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
404
405   return "One of first name, last name, or title must have a value"
406     if ! grep $self->$_(), qw( first last title);
407
408   $self->SUPER::check;
409 }
410
411 sub line {
412   my $self = shift;
413   my $data = $self->first. ' '. $self->last;
414   $data .= ', '. $self->title
415     if $self->title;
416   $data .= ' ('. $self->comment. ')'
417     if $self->comment;
418   $data;
419 }
420
421 sub contact_classname {
422   my $self = shift;
423   my $contact_class = $self->contact_class or return '';
424   $contact_class->classname;
425 }
426
427 =back
428
429 =head1 BUGS
430
431 =head1 SEE ALSO
432
433 L<FS::Record>, schema.html from the base documentation.
434
435 =cut
436
437 1;
438