msg_template improvements, RT#8324
[freeside.git] / FS / FS / msg_template.pm
1 package FS::msg_template;
2
3 use strict;
4 use base qw( FS::Record );
5 use Text::Template;
6 use FS::Misc qw( generate_email send_email );
7 use FS::Conf;
8 use FS::Record qw( qsearch qsearchs );
9
10 use Date::Format qw( time2str );
11 use HTML::Entities qw( encode_entities) ;
12 use vars '$DEBUG';
13
14 $DEBUG=1;
15
16 =head1 NAME
17
18 FS::msg_template - Object methods for msg_template records
19
20 =head1 SYNOPSIS
21
22   use FS::msg_template;
23
24   $record = new FS::msg_template \%hash;
25   $record = new FS::msg_template { 'column' => 'value' };
26
27   $error = $record->insert;
28
29   $error = $new_record->replace($old_record);
30
31   $error = $record->delete;
32
33   $error = $record->check;
34
35 =head1 DESCRIPTION
36
37 An FS::msg_template object represents a customer message template.
38 FS::msg_template inherits from FS::Record.  The following fields are currently
39 supported:
40
41 =over 4
42
43 =item msgnum
44
45 primary key
46
47 =item msgname
48
49 Template name.
50
51 =item agentnum
52
53 Agent associated with this template.  Can be NULL for a global template.
54
55 =item mime_type
56
57 MIME type.  Defaults to text/html.
58
59 =item from_addr
60
61 Source email address.
62
63 =item subject
64
65 The message subject line, in L<Text::Template> format.
66
67 =item body
68
69 The message body, as plain text or HTML, in L<Text::Template> format.
70
71 =item disabled
72
73 disabled
74
75 =back
76
77 =head1 METHODS
78
79 =over 4
80
81 =item new HASHREF
82
83 Creates a new template.  To add the template to the database, see L<"insert">.
84
85 Note that this stores the hash reference, not a distinct copy of the hash it
86 points to.  You can ask the object for a copy with the I<hash> method.
87
88 =cut
89
90 # the new method can be inherited from FS::Record, if a table method is defined
91
92 sub table { 'msg_template'; }
93
94 =item insert
95
96 Adds this record to the database.  If there is an error, returns the error,
97 otherwise returns false.
98
99 =cut
100
101 # the insert method can be inherited from FS::Record
102
103 =item delete
104
105 Delete this record from the database.
106
107 =cut
108
109 # the delete method can be inherited from FS::Record
110
111 =item replace OLD_RECORD
112
113 Replaces the OLD_RECORD with this one in the database.  If there is an error,
114 returns the error, otherwise returns false.
115
116 =cut
117
118 # the replace method can be inherited from FS::Record
119
120 =item check
121
122 Checks all fields to make sure this is a valid template.  If there is
123 an error, returns the error, otherwise returns false.  Called by the insert
124 and replace methods.
125
126 =cut
127
128 # the check method should currently be supplied - FS::Record contains some
129 # data checking routines
130
131 sub check {
132   my $self = shift;
133
134   my $error = 
135     $self->ut_numbern('msgnum')
136     || $self->ut_text('msgname')
137     || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
138     || $self->ut_textn('mime_type')
139     || $self->ut_anything('subject')
140     || $self->ut_anything('body')
141     || $self->ut_enum('disabled', [ '', 'Y' ] )
142     || $self->ut_textn('from_addr')
143   ;
144   return $error if $error;
145
146   my $body = $self->body;
147   $body =~ s/&nbsp;/ /g; # just in case these somehow get in
148   $self->body($body);
149
150   $self->mime_type('text/html') unless $self->mime_type;
151
152   $self->SUPER::check;
153 }
154
155 =item prepare OPTION => VALUE
156
157 Fills in the template and returns a hash of the 'from' address, 'to' 
158 addresses, subject line, and body.
159
160 Options are passed as a list of name/value pairs:
161
162 =over 4
163
164 =item cust_main
165
166 Customer object (required).
167
168 =item object
169
170 Additional context object (currently, can be a cust_main object, cust_pkg
171 object, or cust_bill object).
172
173 =back
174
175 =cut
176
177 sub prepare {
178   my( $self, %opt ) = @_;
179
180   my $cust_main = $opt{'cust_main'};
181   my $object = $opt{'object'};
182   warn "preparing template '".$self->msgname."' to cust#".$cust_main->custnum."\n"
183     if($DEBUG);
184
185   my $subs = $self->substitutions;
186
187   ###
188   # create substitution table
189   ###  
190   my %hash;
191   foreach my $obj ($cust_main, $object || ()) {
192     foreach my $name (@{ $subs->{$obj->table} }) {
193       if(!ref($name)) {
194         # simple case
195         $hash{$name} = $obj->$name();
196       }
197       elsif( ref($name) eq 'ARRAY' ) {
198         # [ foo => sub { ... } ]
199         $hash{$name->[0]} = $name->[1]->($obj);
200       }
201       else {
202         warn "bad msg_template substitution: '$name'\n";
203         #skip it?
204       } 
205     } 
206   } 
207   $_ = encode_entities($_) foreach values(%hash); # HTML escape
208
209   ###
210   # fill-in
211   ###
212
213   my $subject_tmpl = new Text::Template (
214     TYPE   => 'STRING',
215     SOURCE => $self->subject,
216   );
217   my $subject = $subject_tmpl->fill_in( HASH => \%hash );
218
219   my $body_tmpl = new Text::Template (
220     TYPE   => 'STRING',
221     SOURCE => $self->body,
222   );
223   my $body = $body_tmpl->fill_in( HASH => \%hash );
224
225   ###
226   # and email
227   ###
228
229   my @to = $cust_main->invoicing_list_emailonly;
230   #unless (@to) { #XXX do something }
231
232   my $conf = new FS::Conf;
233
234   (
235     'from' => $self->from || 
236               scalar( $conf->config('invoice_from', $cust_main->agentnum) ),
237     'to'   => \@to,
238     'subject'   => $subject,
239     'html_body' => $body,
240     #XXX auto-make a text copy w/HTML::FormatText?
241     #  alas, us luddite mutt/pine users just aren't that big a deal
242   );
243
244 }
245
246 =item send OPTION => VALUE
247
248 Fills in the template and sends it to the customer.  Options are as for 
249 'prepare'.
250
251 =cut
252
253 sub send {
254   my $self = shift;
255   send_email(generate_email($self->prepare(@_)));
256 }
257
258 # helper sub for package dates
259 my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' };
260
261 #return contexts and fill-in values
262 # If you add anything, be sure to add a description in 
263 # httemplate/edit/msg_template.html.
264 sub substitutions {
265   { 'cust_main' => [qw(
266       display_custnum agentnum agent_name
267
268       last first company
269       name name_short contact contact_firstlast
270       address1 address2 city county state zip
271       country
272       daytime night fax
273
274       has_ship_address
275       ship_last ship_first ship_company
276       ship_name ship_name_short ship_contact ship_contact_firstlast
277       ship_address1 ship_address2 ship_city ship_county ship_state ship_zip
278       ship_country
279       ship_daytime ship_night ship_fax
280
281       payby paymask payname paytype payip
282       num_cancelled_pkgs num_ncancelled_pkgs num_pkgs
283       classname categoryname
284       balance
285       invoicing_list_emailonly
286       cust_status ucfirst_cust_status cust_statuscolor
287
288       signupdate dundate
289       ),
290       [ signupdate_ymd    => sub { time2str('%Y-%m-%d', shift->signupdate) } ],
291       [ dundate_ymd       => sub { time2str('%Y-%m-%d', shift->dundate) } ],
292       [ paydate_my        => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ],
293       [ otaker_first      => sub { shift->access_user->first } ],
294       [ otaker_last       => sub { shift->access_user->last } ],
295     ],
296     # next_bill_date
297     'cust_pkg'  => [qw( 
298       pkgnum pkg_label pkg_label_long
299       location_label
300       status statuscolor
301     
302       start_date setup bill last_bill 
303       adjourn susp expire 
304       labels_short
305       ),
306       [ cancel            => sub { shift->getfield('cancel') } ], # grrr...
307       [ start_ymd         => sub { $ymd->(shift->getfield('start_date')) } ],
308       [ setup_ymd         => sub { $ymd->(shift->getfield('setup')) } ],
309       [ next_bill_ymd     => sub { $ymd->(shift->getfield('bill')) } ],
310       [ last_bill_ymd     => sub { $ymd->(shift->getfield('last_bill')) } ],
311       [ adjourn_ymd       => sub { $ymd->(shift->getfield('adjourn')) } ],
312       [ susp_ymd          => sub { $ymd->(shift->getfield('susp')) } ],
313       [ expire_ymd        => sub { $ymd->(shift->getfield('expire')) } ],
314       [ cancel_ymd        => sub { $ymd->(shift->getfield('cancel')) } ],
315     ],
316     'cust_bill' => [qw(
317       invnum
318     )],
319     #XXX not really thinking about cust_bill substitutions quite yet
320     
321     'svc_acct' => [qw(
322       username
323       ),
324       [ password          => sub { shift->getfield('_password') } ],
325     ], # for welcome messages
326   };
327 }
328
329 sub _upgrade_data {
330   my ($self, %opts) = @_;
331
332   my @fixes = (
333     [ 'alerter_msgnum',  'alerter_template',   '',               '' ],
334     [ 'cancel_msgnum',   'cancelmessage',      'cancelsubject',  '' ],
335     [ 'decline_msgnum',  'declinetemplate',    '',               '' ],
336     [ 'impending_recur_msgnum', 'impending_recur_template', '',  '' ],
337     [ 'welcome_msgnum',  'welcome_email',      'welcome_email-subject', 'welcome_email-from' ],
338     [ 'warning_msgnum',  'warning_email',      'warning_email-subject', 'warning_email-from' ],
339   );
340  
341   my $conf = new FS::Conf;
342   my @agentnums = ('', map {$_->agentnum} qsearch('agent', {}));
343   foreach my $agentnum (@agentnums) {
344     foreach (@fixes) {
345       my ($newname, $oldname, $subject, $from) = @$_;
346       if ($conf->exists($oldname, $agentnum)) {
347         my $new = new FS::msg_template({
348            'msgname'   => $oldname,
349            'agentnum'  => $agentnum,
350            'from_addr' => ($from && $conf->config($from, $agentnum)) || 
351                           $conf->config('invoice_from', $agentnum),
352            'subject'   => ($subject && $conf->config($subject, $agentnum)) || '',
353            'mime_type' => 'text/html',
354            'body'      => join('<BR>',$conf->config($oldname, $agentnum)),
355         });
356         my $error = $new->insert;
357         die $error if $error;
358         $conf->set($newname, $new->msgnum, $agentnum);
359         $conf->delete($oldname, $agentnum);
360         $conf->delete($from, $agentnum) if $from;
361         $conf->delete($subject, $agentnum) if $subject;
362       }
363     }
364   }
365 }
366
367 =back
368
369 =head1 BUGS
370
371 =head1 SEE ALSO
372
373 L<FS::Record>, schema.html from the base documentation.
374
375 =cut
376
377 1;
378