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