svc_broadband merge
[freeside.git] / FS / FS / cust_svc.pm
1 package FS::cust_svc;
2
3 use strict;
4 use vars qw( @ISA );
5 use Carp qw( cluck );
6 use FS::Record qw( qsearch qsearchs dbh );
7 use FS::cust_pkg;
8 use FS::part_pkg;
9 use FS::part_svc;
10 use FS::pkg_svc;
11 use FS::svc_acct;
12 use FS::svc_acct_sm;
13 use FS::svc_domain;
14 use FS::svc_forward;
15 use FS::domain_record;
16
17 @ISA = qw( FS::Record );
18
19 sub _cache {
20   my $self = shift;
21   my ( $hashref, $cache ) = @_;
22   if ( $hashref->{'username'} ) {
23     $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
24   }
25   if ( $hashref->{'svc'} ) {
26     $self->{'_svcpart'} = FS::part_svc->new($hashref);
27   }
28 }
29
30 =head1 NAME
31
32 FS::cust_svc - Object method for cust_svc objects
33
34 =head1 SYNOPSIS
35
36   use FS::cust_svc;
37
38   $record = new FS::cust_svc \%hash
39   $record = new FS::cust_svc { 'column' => 'value' };
40
41   $error = $record->insert;
42
43   $error = $new_record->replace($old_record);
44
45   $error = $record->delete;
46
47   $error = $record->check;
48
49   ($label, $value) = $record->label;
50
51 =head1 DESCRIPTION
52
53 An FS::cust_svc represents a service.  FS::cust_svc inherits from FS::Record.
54 The following fields are currently supported:
55
56 =over 4
57
58 =item svcnum - primary key (assigned automatically for new services)
59
60 =item pkgnum - Package (see L<FS::cust_pkg>)
61
62 =item svcpart - Service definition (see L<FS::part_svc>)
63
64 =back
65
66 =head1 METHODS
67
68 =over 4
69
70 =item new HASHREF
71
72 Creates a new service.  To add the refund to the database, see L<"insert">.
73 Services are normally created by creating FS::svc_ objects (see
74 L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
75
76 =cut
77
78 sub table { 'cust_svc'; }
79
80 =item insert
81
82 Adds this service to the database.  If there is an error, returns the error,
83 otherwise returns false.
84
85 =item delete
86
87 Deletes this service from the database.  If there is an error, returns the
88 error, otherwise returns false.  Note that this only removes the cust_svc
89 record - you should probably use the B<cancel> method instead.
90
91 =item cancel
92
93 Cancels the relevant service by calling the B<cancel> method of the associated
94 FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
95 deleting the FS::svc_XXX record and then deleting this record.
96
97 If there is an error, returns the error, otherwise returns false.
98
99 =cut
100
101 sub cancel {
102   my $self = shift;
103
104   local $SIG{HUP} = 'IGNORE';
105   local $SIG{INT} = 'IGNORE';
106   local $SIG{QUIT} = 'IGNORE'; 
107   local $SIG{TERM} = 'IGNORE';
108   local $SIG{TSTP} = 'IGNORE';
109   local $SIG{PIPE} = 'IGNORE';
110
111   my $oldAutoCommit = $FS::UID::AutoCommit;
112   local $FS::UID::AutoCommit = 0;
113   my $dbh = dbh;
114
115   my $part_svc = $self->part_svc;
116
117   $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
118     $dbh->rollback if $oldAutoCommit;
119     return "Illegal svcdb value in part_svc!";
120   };
121   my $svcdb = $1;
122   require "FS/$svcdb.pm";
123
124   my $svc = $self->svc_x;
125   if ($svc) {
126     my $error = $svc->cancel;
127     if ( $error ) {
128       $dbh->rollback if $oldAutoCommit;
129       return "Error canceling service: $error";
130     }
131     $error = $svc->delete;
132     if ( $error ) {
133       $dbh->rollback if $oldAutoCommit;
134       return "Error deleting service: $error";
135     }
136   }
137
138   my $error = $self->delete;
139   if ( $error ) {
140     $dbh->rollback if $oldAutoCommit;
141     return "Error deleting cust_svc: $error";
142   }
143
144   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
145
146   ''; #no errors
147
148 }
149
150 =item replace OLD_RECORD
151
152 Replaces the OLD_RECORD with this one in the database.  If there is an error,
153 returns the error, otherwise returns false.
154
155 =cut
156
157 sub replace {
158   my ( $new, $old ) = ( shift, shift );
159
160   local $SIG{HUP} = 'IGNORE';
161   local $SIG{INT} = 'IGNORE';
162   local $SIG{QUIT} = 'IGNORE';
163   local $SIG{TERM} = 'IGNORE';
164   local $SIG{TSTP} = 'IGNORE';
165   local $SIG{PIPE} = 'IGNORE';
166
167   my $oldAutoCommit = $FS::UID::AutoCommit;
168   local $FS::UID::AutoCommit = 0;
169   my $dbh = dbh;
170
171   my $error = $new->SUPER::replace($old);
172   if ( $error ) {
173     $dbh->rollback if $oldAutoCommit;
174     return $error if $error;
175   }
176
177   if ( $new->svcpart != $old->svcpart ) {
178     my $svc_x = $new->svc_x;
179     my $new_svc_x = ref($svc_x)->new({$svc_x->hash});
180     my $error = $new_svc_x->replace($svc_x);
181     if ( $error ) {
182       $dbh->rollback if $oldAutoCommit;
183       return $error if $error;
184     }
185   }
186
187   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
188   ''; #no error
189
190 }
191
192 =item check
193
194 Checks all fields to make sure this is a valid service.  If there is an error,
195 returns the error, otehrwise returns false.  Called by the insert and
196 replace methods.
197
198 =cut
199
200 sub check {
201   my $self = shift;
202
203   my $error =
204     $self->ut_numbern('svcnum')
205     || $self->ut_numbern('pkgnum')
206     || $self->ut_number('svcpart')
207   ;
208   return $error if $error;
209
210   my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
211   return "Unknown svcpart" unless $part_svc;
212
213   if ( $self->pkgnum ) {
214     my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
215     return "Unknown pkgnum" unless $cust_pkg;
216     my $pkg_svc = qsearchs( 'pkg_svc', {
217       'pkgpart' => $cust_pkg->pkgpart,
218       'svcpart' => $self->svcpart,
219     });
220     # or new FS::pkg_svc ( { 'pkgpart'  => $cust_pkg->pkgpart,
221     #                        'svcpart'  => $self->svcpart,
222     #                        'quantity' => 0                   } );
223
224     my @cust_svc = qsearch('cust_svc', {
225       'pkgnum'  => $self->pkgnum,
226       'svcpart' => $self->svcpart,
227     });
228     return "Already ". scalar(@cust_svc). " ". $part_svc->svc.
229            " services for pkgnum ". $self->pkgnum
230       if scalar(@cust_svc) >= $pkg_svc->quantity;
231   }
232
233   ''; #no error
234 }
235
236 =item part_svc
237
238 Returns the definition for this service, as a FS::part_svc object (see
239 L<FS::part_svc>).
240
241 =cut
242
243 sub part_svc {
244   my $self = shift;
245   $self->{'_svcpart'}
246     ? $self->{'_svcpart'}
247     : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
248 }
249
250 =item cust_pkg
251
252 Returns the definition for this service, as a FS::part_svc object (see
253 L<FS::part_svc>).
254
255 =cut
256
257 sub cust_pkg {
258   my $self = shift;
259   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
260 }
261
262 =item label
263
264 Returns a list consisting of:
265 - The name of this service (from part_svc)
266 - A meaningful identifier (username, domain, or mail alias)
267 - The table name (i.e. svc_domain) for this service
268
269 =cut
270
271 sub label {
272   my $self = shift;
273   my $svcdb = $self->part_svc->svcdb;
274   my $svc_x = $self->svc_x
275     or die "can't find $svcdb.svcnum ". $self->svcnum;
276   my $tag;
277   if ( $svcdb eq 'svc_acct' ) {
278     $tag = $svc_x->email;
279   } elsif ( $svcdb eq 'svc_acct_sm' ) {
280     my $domuser = $svc_x->domuser eq '*' ? '(anything)' : $svc_x->domuser;
281     my $svc_domain = qsearchs ( 'svc_domain', { 'svcnum' => $svc_x->domsvc } );
282     my $domain = $svc_domain->domain;
283     $tag = "$domuser\@$domain";
284   } elsif ( $svcdb eq 'svc_forward' ) {
285     my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->srcsvc } );
286     $tag = $svc_acct->email. '->';
287     if ( $svc_x->dstsvc ) {
288       $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->dstsvc } );
289       $tag .= $svc_acct->email;
290     } else {
291       $tag .= $svc_x->dst;
292     }
293   } elsif ( $svcdb eq 'svc_domain' ) {
294     $tag = $svc_x->getfield('domain');
295   } elsif ( $svcdb eq 'svc_www' ) {
296     my $domain = qsearchs( 'domain_record', { 'recnum' => $svc_x->recnum } );
297     $tag = $domain->reczone;
298   } elsif ( $svcdb eq 'svc_broadband' ) {
299     $tag = $svc_x->ip_addr . '/' . $svc_x->ip_netmask;
300   } else {
301     cluck "warning: asked for label of unsupported svcdb; using svcnum";
302     $tag = $svc_x->getfield('svcnum');
303   }
304   $self->part_svc->svc, $tag, $svcdb;
305 }
306
307 =item svc_x
308
309 Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
310 FS::svc_domain object, etc.)
311
312 =cut
313
314 sub svc_x {
315   my $self = shift;
316   my $svcdb = $self->part_svc->svcdb;
317   if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
318     $self->{'_svc_acct'};
319   } else {
320     qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
321   }
322 }
323
324 =item seconds_since TIMESTAMP
325
326 See L<FS::svc_acct/seconds_since>.  Equivalent to
327 $cust_svc->svc_x->seconds_since, but more efficient.  Meaningless for records
328 where B<svcdb> is not "svc_acct".
329
330 =cut
331
332 #note: implementation here, POD in FS::svc_acct
333 sub seconds_since {
334   my($self, $since) = @_;
335   my $dbh = dbh;
336   my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
337                               WHERE svcnum = ?
338                                 AND login >= ?
339                                 AND logout IS NOT NULL'
340   ) or die $dbh->errstr;
341   $sth->execute($self->svcnum, $since) or die $sth->errstr;
342   $sth->fetchrow_arrayref->[0];
343 }
344
345 =back
346
347 =head1 VERSION
348
349 $Id: cust_svc.pm,v 1.16 2002-09-09 23:01:35 khoff Exp $
350
351 =head1 BUGS
352
353 Behaviour of changing the svcpart of cust_svc records is undefined and should
354 possibly be prohibited, and pkg_svc records are not checked.
355
356 pkg_svc records are not checked in general (here).
357
358 Deleting this record doesn't check or delete the svc_* record associated
359 with this record.
360
361 =head1 SEE ALSO
362
363 L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>, 
364 schema.html from the base documentation
365
366 =cut
367
368 1;
369