From 755159a8654a2eda89badd1498f8def3a472cb15 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 30 Apr 2016 18:07:50 -0700 Subject: allow sending email to specific contact classes, #33316 Conflicts: FS/FS/cust_main_Mixin.pm FS/FS/msg_template/email.pm httemplate/misc/email-customers.html --- FS/FS/cust_main.pm | 67 ++++++++++++++++++++++++++++++++++++ FS/FS/cust_main_Mixin.pm | 24 +++++++++++-- FS/FS/msg_template.pm | 22 +++++++++--- httemplate/elements/checkboxes.html | 6 +++- httemplate/misc/email-customers.html | 56 +++++++++++++++++++++++++++--- 5 files changed, 163 insertions(+), 12 deletions(-) diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index cfcd5f43f..ef3ab61a7 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3363,6 +3363,73 @@ sub invoicing_list_emailonly_scalar { join(', ', $self->invoicing_list_emailonly); } +=item contact_list [ CLASSNUM, ... ] + +Returns a list of contacts (L objects) for the customer. If +a list of contact classnums is given, returns only contacts in those +classes. If the pseudo-classnum 'invoice' is given, returns contacts that +are marked as invoice destinations. If '0' is given, also returns contacts +with no class. + +If no arguments are given, returns all contacts for the customer. + +=cut + +sub contact_list { + my $self = shift; + my $search = { + table => 'contact', + select => 'contact.*, cust_contact.invoice_dest', + addl_from => ' JOIN cust_contact USING (contactnum)', + extra_sql => ' WHERE cust_contact.custnum = '.$self->custnum, + }; + + my @orwhere; + my @classnums; + foreach (@_) { + if ( $_ eq 'invoice' ) { + push @orwhere, 'cust_contact.invoice_dest = \'Y\''; + } elsif ( $_ eq '0' ) { + push @orwhere, 'cust_contact.classnum is null'; + } elsif ( /^\d+$/ ) { + push @classnums, $_; + } else { + die "bad classnum argument '$_'"; + } + } + + if (@classnums) { + push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')'; + } + if (@orwhere) { + $search->{extra_sql} .= ' AND (' . + join(' OR ', map "( $_ )", @orwhere) . + ')'; + } + + qsearch($search); +} + +=item contact_list_email [ CLASSNUM, ... ] + +Same as L, but returns email destinations instead of contact +objects. + +=cut + +sub contact_list_email { + my $self = shift; + my @contacts = $self->contact_list(@_); + my @emails; + foreach my $contact (@contacts) { + foreach my $contact_email ($contact->contact_email) { + push @emails, + $contact->firstlast . ' <' . $contact_email->emailaddress . '>'; + } + } + @emails; +} + =item referral_custnum_cust_main Returns the customer who referred this customer (or the empty string, if diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 96e520d52..94e6eaa29 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -380,6 +380,12 @@ HTML body Text body +=item to_contact_classnum + +The customer contact class (or classes, as a comma-separated list) to send +the message to. If unspecified, will be sent to any contacts that are marked +as invoice destinations (the equivalent of specifying 'invoice'). + =back Returns an error message, or false for success. @@ -403,6 +409,7 @@ sub email_search_result { my $subject = delete $param->{subject}; my $html_body = delete $param->{html_body}; my $text_body = delete $param->{text_body}; + my $to_contact_classnum = delete $param->{to_contact_classnum}; my $error = ''; my $job = delete $param->{'job'} @@ -455,10 +462,21 @@ sub email_search_result { %message = $msg_template->prepare( 'cust_main' => $cust_main, 'object' => $obj, + 'to_contact_classnum' => $to_contact_classnum, ); - } - else { - my @to = $cust_main->invoicing_list_emailonly; + + } else { + # 3.x: false laziness with msg_template.pm; on 4.x, all email notices + # are generated from templates and this case goes away + my @classes; + if ( $opt{'to_contact_classnum'} ) { + my $classnum = $opt{'to_contact_classnum'}; + @classes = ref($classnum) ? @$classnum : split(',', $classnum); + } + if (!@classes) { + @classes = ( 'invoice' ); + } + my @to = $cust_main->contact_list_email(@classes); next if !@to; %message = ( diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 70e556924..50a9b3f27 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -383,12 +383,26 @@ sub prepare { my @to; if ( exists($opt{'to'}) ) { + @to = split(/\s*,\s*/, $opt{'to'}); + + } elsif ( $cust_main ) { + + my @classes; + if ( $opt{'to_contact_classnum'} ) { + my $classnum = $opt{'to_contact_classnum'}; + @classes = ref($classnum) ? @$classnum : split(',', $classnum); + } + if (!@classes) { + @classes = ( 'invoice' ); + } + @to = $cust_main->contact_list_email(@classes); + + } else { + + die 'no To: address or cust_main object specified'; + } - else { - @to = $cust_main->invoicing_list_emailonly; - } - # no warning when preparing with no destination my $from_addr = $self->from_addr; diff --git a/httemplate/elements/checkboxes.html b/httemplate/elements/checkboxes.html index 69ef18fb9..1f342241d 100644 --- a/httemplate/elements/checkboxes.html +++ b/httemplate/elements/checkboxes.html @@ -27,7 +27,7 @@ Example: - +
> % unless ( $opt{'disable_links'} ) { @@ -108,4 +108,8 @@ $opt{'error_checked_callback'} ||= sub { $cgi->param($opt{'element_name_prefix'}. $name ); }; +my $style = ''; +if ($opt{'style'}) { + $style = 'STYLE="' . $opt{'style'} . '"'; +} diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index cd4c92f23..11ab050b6 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -6,8 +6,8 @@ frozen hash in the 'search' cgi param. Form allows selecting an existing msg_te or creating a custom message, and shows a preview of the message before sending. If linked to as a popup, include the cgi parameter 'popup' for proper header handling. -This may also be used as an element in other pages, enabling you to provide an -alternate initial form while using this for search freezing/thawing and +This may also be used as an element in other pages, enabling you to provide +an alternate initial form while using this for search freezing/thawing and preview/send actions, with the following options: acl - the access right to use (defaults to 'Bulk send customer notices') @@ -46,6 +46,7 @@ should be used to set msgnum or from/subject/html_body cgi params + % if ( $cgi->param('action') eq 'send' ) { @@ -53,7 +54,8 @@ should be used to set msgnum or from/subject/html_body cgi params <& /elements/progress-init.html, 'OneTrueForm', - [ qw( search table from subject html_body text_body msgnum ) ], + [ qw( search table from subject html_body text_body + msgnum to_contact_classnum ) ], $process_url, $pdest, &> @@ -68,7 +70,6 @@ should be used to set msgnum or from/subject/html_body cgi params
- % if ( $msg_template ) { <% include('/elements/tr-fixed.html', 'label' => 'Template:', @@ -84,6 +85,10 @@ should be used to set msgnum or from/subject/html_body cgi params ) %> + <& /elements/tr-td-label.html, 'label' => 'To contacts:' &> + + + <% include('/elements/tr-fixed.html', 'field' => 'subject', 'label' => 'Subject:', @@ -144,6 +149,21 @@ Template: onchange => 'toggle(this)', &>
+% } +% # select destination contact classes +Send to contacts: + <& /elements/checkboxes.html, + 'style' => 'display: inline; vertical-align: top', + 'disable_links' => 1, + 'names_list' => \@contact_checkboxes, + 'element_name_prefix' => 'contact_class_', + 'checked_callback' => sub { + my($cgi, $name) = @_; + $name eq 'invoice' #others default to unchecked + }, + &> +
+% # if sending a one-off message, show a form to edit it
<% join('
', @contact_classname) %>
<& /elements/tr-td-label.html, 'label' => 'From:' &>
<& /elements/input-text.html, @@ -250,6 +270,12 @@ if ( $cgi->param('from') ) { my $subject = $cgi->param('subject') || ''; my $html_body = $cgi->param('html_body') || ''; +my @contact_classnum; +my @contact_classname; + +my $subject = $cgi->param('subject'); +my $body = $cgi->param('body'); + my $msg_template = ''; if ( $cgi->param('action') eq 'preview' ) { @@ -279,6 +305,28 @@ if ( $cgi->param('action') eq 'preview' ) { my %message = $msg_template->prepare(%msgopts); ($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'}; } + + # contact_class_X params + foreach my $param ( $cgi->multi_param ) { + if ( $param =~ /^contact_class_(\w+)$/ ) { + push @contact_classnum, $1; + if ( $1 eq 'invoice' ) { + push @contact_classname, 'Invoice recipients'; + } else { + my $contact_class = FS::contact_class->by_key($1); + push @contact_classname, encode_entities($contact_class->classname); + } + } + } } +my @contact_checkboxes = ( + [ 'invoice' => { label => 'Invoice recipients' } ] +); +foreach my $class (qsearch('contact_class', { disabled => '' })) { + push @contact_checkboxes, [ + $class->classnum, + { label => $class->classname } + ]; +} -- cgit v1.2.1 From eb1796299a429ba5cd3b939a65bf5e95abf0086c Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Tue, 3 May 2016 12:52:21 -0700 Subject: email to specific contact classes, 3.x adjustments --- FS/FS/Misc.pm | 7 +++++ FS/FS/cust_main.pm | 56 ++++++++++++++++++++++++------------ FS/FS/cust_main_Mixin.pm | 5 ++-- httemplate/misc/email-customers.html | 10 ++++--- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index e425c4a4b..eedc736ee 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -256,6 +256,13 @@ sub send_email { push @to, $options{bcc} if defined($options{bcc}); # make sure my @env_to = split(/\s*,\s*/, join(', ', @to)); + # strip display-name from envelope addresses + foreach (@env_to) { + s/^\s*//; + s/\s*$//; + s/^(.*)\s*<(.*@.*)>$/$2/; + } + local $@; # just in case eval { sendmail($message, { transport => $transport, from => $from, diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index ef3ab61a7..41b274320 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -3367,9 +3367,7 @@ sub invoicing_list_emailonly_scalar { Returns a list of contacts (L objects) for the customer. If a list of contact classnums is given, returns only contacts in those -classes. If the pseudo-classnum 'invoice' is given, returns contacts that -are marked as invoice destinations. If '0' is given, also returns contacts -with no class. +classes. If '0' is given, also returns contacts with no class. If no arguments are given, returns all contacts for the customer. @@ -3379,18 +3377,15 @@ sub contact_list { my $self = shift; my $search = { table => 'contact', - select => 'contact.*, cust_contact.invoice_dest', - addl_from => ' JOIN cust_contact USING (contactnum)', - extra_sql => ' WHERE cust_contact.custnum = '.$self->custnum, + select => 'contact.*', + extra_sql => ' WHERE contact.custnum = '.$self->custnum, }; my @orwhere; my @classnums; foreach (@_) { - if ( $_ eq 'invoice' ) { - push @orwhere, 'cust_contact.invoice_dest = \'Y\''; - } elsif ( $_ eq '0' ) { - push @orwhere, 'cust_contact.classnum is null'; + if ( $_ eq '0' ) { + push @orwhere, 'contact.classnum is null'; } elsif ( /^\d+$/ ) { push @classnums, $_; } else { @@ -3399,7 +3394,7 @@ sub contact_list { } if (@classnums) { - push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')'; + push @orwhere, 'contact.classnum IN ('.join(',', @classnums).')'; } if (@orwhere) { $search->{extra_sql} .= ' AND (' . @@ -3413,21 +3408,44 @@ sub contact_list { =item contact_list_email [ CLASSNUM, ... ] Same as L, but returns email destinations instead of contact -objects. +objects. Also accepts 'invoice' as an argument, in which case this will also +return the invoice email address if any. =cut sub contact_list_email { my $self = shift; - my @contacts = $self->contact_list(@_); - my @emails; - foreach my $contact (@contacts) { - foreach my $contact_email ($contact->contact_email) { - push @emails, - $contact->firstlast . ' <' . $contact_email->emailaddress . '>'; + my @classnums; + my $and_invoice; + foreach (@_) { + if (/^invoice$/) { + $and_invoice = 1; + } else { + push @classnums, $_; + } + } + my %emails; + # if the only argument passed was 'invoice' then no classnums are + # intended, so skip this. + if ( @classnums ) { + my @contacts = $self->contact_list(@classnums); + foreach my $contact (@contacts) { + foreach my $contact_email ($contact->contact_email) { + # unlike on 4.x, we have a separate list of invoice email + # destinations. + # make sure they're not redundant with contact emails + my $dest = $contact->firstlast . ' <' . $contact_email->emailaddress . '>'; + $emails{ $contact_email->emailaddress } = $dest; + } + } + } + if ( $and_invoice ) { + foreach my $email ($self->invoicing_list_emailonly) { + my $dest = $self->name_short . ' <' . $email . '>'; + $emails{ $email } ||= $dest; } } - @emails; + values %emails; } =item referral_custnum_cust_main diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index 94e6eaa29..dee9aa831 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -469,9 +469,8 @@ sub email_search_result { # 3.x: false laziness with msg_template.pm; on 4.x, all email notices # are generated from templates and this case goes away my @classes; - if ( $opt{'to_contact_classnum'} ) { - my $classnum = $opt{'to_contact_classnum'}; - @classes = ref($classnum) ? @$classnum : split(',', $classnum); + if ( $to_contact_classnum ) { + @classes = ref($to_contact_classnum) ? @$to_contact_classnum : split(',', $to_contact_classnum); } if (!@classes) { @classes = ( 'invoice' ); diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index 11ab050b6..c9a216c3a 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -46,10 +46,10 @@ should be used to set msgnum or from/subject/html_body cgi params - % if ( $cgi->param('action') eq 'send' ) { + Sending notice <& /elements/progress-init.html, @@ -62,6 +62,7 @@ should be used to set msgnum or from/subject/html_body cgi params % } elsif ( $cgi->param('action') eq 'preview' ) { + Preview notice % } @@ -149,7 +150,6 @@ Template: onchange => 'toggle(this)', &>
-% } % # select destination contact classes Send to contacts: <& /elements/checkboxes.html, @@ -306,8 +306,8 @@ if ( $cgi->param('action') eq 'preview' ) { ($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'}; } - # contact_class_X params - foreach my $param ( $cgi->multi_param ) { + # contact_class_X params in preview + foreach my $param ( $cgi->param ) { if ( $param =~ /^contact_class_(\w+)$/ ) { push @contact_classnum, $1; if ( $1 eq 'invoice' ) { @@ -318,8 +318,10 @@ if ( $cgi->param('action') eq 'preview' ) { } } } + } +# and set up contact checkboxes for edit mode my @contact_checkboxes = ( [ 'invoice' => { label => 'Invoice recipients' } ] ); -- cgit v1.2.1 From 5b2b242ad80a2efac3fb3f4d919142307084bd73 Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Mon, 2 May 2016 21:44:40 -0500 Subject: RT#37632: Credit card validation [v3 fixes] --- FS/FS/cust_main.pm | 24 ++++++++++++++++++++++++ FS/FS/cust_main/Billing_Realtime.pm | 2 ++ 2 files changed, 26 insertions(+) diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 6e666466d..22b7a1648 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -542,6 +542,16 @@ sub insert { } + # validate card (needs custnum already set) + if ( $self->payby =~ /^(CARD|DCRD)$/ + && $conf->exists('business-onlinepayment-verification') ) { + $error = $self->realtime_verify_bop({ 'method'=>'CC' }); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + warn " setting contacts\n" if $DEBUG > 1; @@ -1540,6 +1550,20 @@ sub replace { return $error if $error; if ( $conf->exists('business-onlinepayment-verification') ) { + #need to standardize paydate for this, false laziness with check + my( $m, $y ); + if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) { + ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" ); + } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) { + ( $m, $y ) = ( $2, "19$1" ); + } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) { + ( $m, $y ) = ( $3, "20$2" ); + } else { + return "Illegal expiration date: ". $self->paydate; + } + $m = sprintf('%02d',$m); + $self->paydate("$y-$m-01"); + $error = $self->realtime_verify_bop({ 'method'=>'CC' }); return $error if $error; } diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 90fda5eb2..4c28d0e0e 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -1938,8 +1938,10 @@ sub realtime_verify_bop { ); $reverse->content( 'action' => 'Reverse Authorization', + $self->_bop_auth(\%options), # B:OP + 'amount' => '1.00', 'authorization' => $transaction->authorization, 'order_number' => $ordernum, -- cgit v1.2.1 From ff9e2f92752c65f3bc3f1812111591bbd2cb355f Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Tue, 3 May 2016 23:17:37 -0500 Subject: RT#37632: Credit card validation [cust_pay_pending handling on error] --- FS/FS/cust_main/Billing_Realtime.pm | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 4c28d0e0e..6c0b655a2 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -1971,6 +1971,17 @@ sub realtime_verify_bop { } + } else { # is not success + + # status is 'done' not 'declined', as in _realtime_bop_result + $cust_pay_pending->status('done'); + $cust_pay_pending->statustext( $transaction->error_message || 'Unknown error' ); + # could also record failure_status here, + # but it's not supported by B::OP::vSecureProcessing... + # need a B::OP module with (reverse) auth only to test it with + my $cpp_declined_err = $cust_pay_pending->replace; + return $cpp_declined_err if $cpp_declined_err; + } ### -- cgit v1.2.1