From 56be01c046fe4aad7f9a9aacc41b509483cc05f1 Mon Sep 17 00:00:00 2001 From: jeff Date: Tue, 23 Jan 2007 23:43:00 +0000 Subject: [PATCH] notices before first charge on flat_delayed --- FS/FS.pm | 2 + FS/FS/Conf.pm | 7 +++ FS/FS/Cron/notify.pm | 134 +++++++++++++++++++++++++++++++++++++++++ FS/FS/Schema.pm | 12 ++++ FS/FS/cust_main.pm | 98 +++++++++++++++++++++++++++++- FS/FS/cust_pkg.pm | 14 +++-- FS/FS/cust_pkg_option.pm | 115 +++++++++++++++++++++++++++++++++++ FS/FS/part_pkg/flat_delayed.pm | 9 ++- FS/bin/freeside-daily | 3 + conf/impending_recur_template | 20 ++++++ 10 files changed, 404 insertions(+), 10 deletions(-) create mode 100644 FS/FS/Cron/notify.pm create mode 100644 FS/FS/cust_pkg_option.pm create mode 100644 conf/impending_recur_template diff --git a/FS/FS.pm b/FS/FS.pm index d8999ca5e..b18d7f7b2 100644 --- a/FS/FS.pm +++ b/FS/FS.pm @@ -111,6 +111,8 @@ L - Service class L - Customer package class +L - Customer package option class + L - Customer class L - Invoice destination diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index bae6a7741..3d1289d45 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2030,6 +2030,13 @@ httemplate/docs/config.html 'type' => 'textarea', }, + { + 'key' => 'impending_recur_template', + 'section' => 'billing', + 'description' => 'Template file for alerts about looming first time recurrant billing. See the billing documentation for details. Also see packages with a flat price plan', + 'type' => 'textarea', + }, + ); 1; diff --git a/FS/FS/Cron/notify.pm b/FS/FS/Cron/notify.pm new file mode 100644 index 000000000..579d50aa0 --- /dev/null +++ b/FS/FS/Cron/notify.pm @@ -0,0 +1,134 @@ +package FS::Cron::notify; + +use strict; +use vars qw( @ISA @EXPORT_OK $DEBUG ); +use Exporter; +use FS::UID qw( dbh ); +use FS::Record qw(qsearch); +use FS::cust_main; +use FS::cust_pkg; + +@ISA = qw( Exporter ); +@EXPORT_OK = qw ( notify_flat_delay ); +$DEBUG = 0; + +sub notify_flat_delay { + + my %opt = @_; + + my $oldAutoCommit = $FS::UID::AutoCommit; + $DEBUG = 1 if $opt{'v'}; + + #we're at now now (and later). + my($time) = $^T; + + # select * from cust_pkg where + my $where_pkg = <<"END"; + where ( cancel is null or cancel = 0 ) + and ( bill > 0 ) + and + 0 < ( select count(*) from part_pkg + where cust_pkg.pkgpart = part_pkg.pkgpart + and part_pkg.plan = 'flat_delayed' + and 0 < ( select count (*) from part_pkg_option + where part_pkg.pkgpart = part_pkg_option.pkgpart + and part_pkg_option.optionname = 'recur_notify' + and part_pkg_option.optionvalue > 0 + and 0 <= $time + + cast(part_pkg_option.optionvalue as integer) + * 86400 + - cust_pkg.bill + and ( cust_pkg.expire is null + or cust_pkg.expire > $time + + cast(part_pkg_option.optionvalue as integer) + * 86400 + ) + ) + ) + and + 0 = ( select count(*) from cust_pkg_option + where cust_pkg.pkgnum = cust_pkg_option.pkgnum + and cust_pkg_option.optionname = 'impending_recur_notification_sent' + and cust_pkg_option.optionvalue = 1 + ) +END + + if ($opt{a}) { + $where_pkg .= <cust_main; + my $custnum = $cust_pkg[0]->custnum; + warn "working on $custnum" if $DEBUG; + while (scalar(@cust_pkg)){ + last if ($cust_pkg[0]->custnum != $custnum); + warn "storing information on " . $cust_pkg[0]->pkgnum if $DEBUG; + push @packages, $cust_pkg[0]->part_pkg->pkg; + push @recurdates, $cust_pkg[0]->bill; + push @cust_pkgs, $cust_pkg[0]; + shift @cust_pkg; + } + my $error = + $cust_main->notify( 'impending_recur_template', + 'extra_fields' => { 'packages' => \@packages, + 'recurdates' => \@recurdates, + }, + ); + warn "Error notifying, custnum ". $cust_main->custnum. ": $error" if $error; + + unless ($error) { + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + for (@cust_pkgs) { + my %options = ($_->options, 'impending_recur_notification_sent' => 1 ); + $error = $_->replace( $_, options => \%options ); + if ($error){ + $dbh->rollback or die $dbh->errstr if $oldAutoCommit; + die "Error updating package options for customer". $cust_main->custnum. + ": $error" if $error; + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + } + + @packages = (); + @recurdates = (); + @cust_pkgs = (); + + } + + dbh->commit or die dbh->errstr if $oldAutoCommit; + +} + +1; diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 91af96476..0d67834a0 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -654,6 +654,18 @@ sub tables_hashref { 'index' => [ ['custnum'], ['pkgpart'] ], }, + 'cust_pkg_option' => { + 'columns' => [ + 'optionnum', 'serial', '', '', '', '', + 'pkgnum', 'int', '', '', '', '', + 'optionname', 'varchar', '', $char_d, '', '', + 'optionvalue', 'text', 'NULL', '', '', '', + ], + 'primary_key' => 'optionnum', + 'unique' => [], + 'index' => [ [ 'pkgnum' ], [ 'optionname' ] ], + }, + 'cust_pkg_reason' => { 'columns' => [ 'num', 'serial', '', '', '', '', diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index ea523785a..f6633f5b3 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -1011,7 +1011,9 @@ sub delete { my %hash = $cust_pkg->hash; $hash{'custnum'} = $new_custnum; my $new_cust_pkg = new FS::cust_pkg ( \%hash ); - my $error = $new_cust_pkg->replace($cust_pkg); + my $error = $new_cust_pkg->replace($cust_pkg, + options => { $cust_pkg->options }, + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -1978,12 +1980,14 @@ sub bill { # If $cust_pkg has been modified, update it and create cust_bill_pkg records ### - if ( $cust_pkg->modified ) { + if ( $cust_pkg->modified ) { # hmmm.. and if the options are modified? warn " package ". $cust_pkg->pkgnum. " modified; updating\n" if $DEBUG >1; - $error=$cust_pkg->replace($old_cust_pkg); + $error=$cust_pkg->replace($old_cust_pkg, + options => { $cust_pkg->options }, + ); if ( $error ) { #just in case $dbh->rollback if $oldAutoCommit; return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"; @@ -4677,6 +4681,94 @@ sub batch_charge { } +=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS + +Sends a templated email notification to the customer (see L - the email sender (default is invoice_from) + +I - comma-separated scalar or arrayref of recipients + (default is invoicing_list) + +I - The subject line of the sent email notification + (default is "Notice from company_name") + +I - a hashref of name/value pairs which will be substituted + into the template + +The following variables are vavailable in the template. + +I<$first> - the customer first name +I<$last> - the customer last name +I<$company> - the customer company +I<$payby> - a description of the method of payment for the customer + # would be nice to use FS::payby::shortname +I<$payinfo> - the account information used to collect for this customer +I<$expdate> - the expiration of the customer payment in seconds from epoch + +=cut + +sub notify { + my ($customer, $template, %options) = @_; + + return unless $conf->exists($template); + + my $from = $conf->config('invoice_from') if $conf->exists('invoice_from'); + $from = $options{from} if exists($options{from}); + + my $to = join(',', $customer->invoicing_list_emailonly); + $to = $options{to} if exists($options{to}); + + my $subject = "Notice from " . $conf->config('company_name') + if $conf->exists('company_name'); + $subject = $options{subject} if exists($options{subject}); + + my $notify_template = new Text::Template (TYPE => 'ARRAY', + SOURCE => [ map "$_\n", + $conf->config($template)] + ) + or die "can't create new Text::Template object: Text::Template::ERROR"; + $notify_template->compile() + or die "can't compile template: Text::Template::ERROR"; + + my $paydate = $customer->paydate; + $FS::notify_template::_template::first = $customer->first; + $FS::notify_template::_template::last = $customer->last; + $FS::notify_template::_template::company = $customer->company; + $FS::notify_template::_template::payinfo = $customer->mask_payinfo; + my $payby = $customer->payby; + my ($payyear,$paymonth,$payday) = split (/-/,$paydate); + my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear); + + #credit cards expire at the end of the month/year of their exp date + if ($payby eq 'CARD' || $payby eq 'DCRD') { + $FS::notify_template::_template::payby = 'credit card'; + ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++); + $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear); + $expire_time--; + }elsif ($payby eq 'COMP') { + $FS::notify_template::_template::payby = 'complimentary account'; + }else{ + $FS::notify_template::_template::payby = 'current method'; + } + $FS::notify_template::_template::expdate = $expire_time; + + for (keys %{$options{extra_fields}}){ + no strict "refs"; + ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_}; + } + + send_email(from => $from, + to => $to, + subject => $subject, + body => $notify_template->fill_in( PACKAGE => + 'FS::notify_template::_template' ), + ); + +} + =back =head1 BUGS diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index fcbb08cd2..c9b454c51 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -30,7 +30,7 @@ use FS::svc_forward; # for sending cancel emails in sub cancel use FS::Conf; -@ISA = qw( FS::cust_main_Mixin FS::Record ); +@ISA = qw( FS::cust_main_Mixin FS::option_Common FS::Record ); $DEBUG = 0; @@ -174,7 +174,7 @@ sub insert { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - my $error = $self->SUPER::insert; + my $error = $self->SUPER::insert($options{options} ? %{$options{options}} : ()); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -318,7 +318,9 @@ sub replace { } - my $error = $new->SUPER::replace($old); + my $error = $new->SUPER::replace($old, + $options{options} ? ${options{options}} : () + ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -483,7 +485,7 @@ sub cancel { my %hash = $self->hash; $hash{'cancel'} = time; my $new = new FS::cust_pkg ( \%hash ); - $error = $new->replace($self); + $error = $new->replace( $self, options => { $self->options } ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -568,7 +570,7 @@ sub suspend { my %hash = $self->hash; $hash{'susp'} = time; my $new = new FS::cust_pkg ( \%hash ); - $error = $new->replace($self); + $error = $new->replace( $self, options => { $self->options } ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; @@ -649,7 +651,7 @@ sub unsuspend { $hash{'susp'} = ''; my $new = new FS::cust_pkg ( \%hash ); - $error = $new->replace($self); + $error = $new->replace( $self, options => { $self->options } ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return $error; diff --git a/FS/FS/cust_pkg_option.pm b/FS/FS/cust_pkg_option.pm new file mode 100644 index 000000000..43a153095 --- /dev/null +++ b/FS/FS/cust_pkg_option.pm @@ -0,0 +1,115 @@ +package FS::cust_pkg_option; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearch qsearchs ); + +@ISA = qw(FS::Record); + +=head1 NAME + +FS::cust_pkg_option - Object methods for cust_pkg_option records + +=head1 SYNOPSIS + + use FS::cust_pkg_option; + + $record = new FS::cust_pkg_option \%hash; + $record = new FS::cust_pkg_option { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_pkg_option object represents an option key an value for a +customer package. FS::cust_pkg_option inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item optionnum - primary key + +=item pkgnum - + +=item optionname - + +=item optionvalue - + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new option. To add the option to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'cust_pkg_option'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid option. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('optionnum') + || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_text('optionname') + || $self->ut_textn('optionvalue') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/part_pkg/flat_delayed.pm b/FS/FS/part_pkg/flat_delayed.pm index ec11699d9..caade409e 100644 --- a/FS/FS/part_pkg/flat_delayed.pm +++ b/FS/FS/part_pkg/flat_delayed.pm @@ -20,12 +20,19 @@ use FS::part_pkg::flat; 'recur_fee' => { 'name' => 'Recurring fee for this package', 'default' => 0, }, + 'recur_notify' => { 'name' => 'Number of days before recurring billing'. + 'commences to notify customer. (0 means '. + 'no warning)', + 'default' => 0, + }, 'unused_credit' => { 'name' => 'Credit the customer for the unused portion'. ' of service at cancellation', 'type' => 'checkbox', }, }, - 'fieldorder' => [ 'free_days', 'setup_fee', 'recur_fee', 'unused_credit' ], + 'fieldorder' => [ 'free_days', 'setup_fee', 'recur_fee', 'recur_notify', + 'unused_credit' + ], #'setup' => '\'my $d = $cust_pkg->bill || $time; $d += 86400 * \' + what.free_days.value + \'; $cust_pkg->bill($d); $cust_pkg_mod_flag=1; \' + what.setup_fee.value', #'recur' => 'what.recur_fee.value', 'weight' => 50, diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily index b9742c4d1..a06a2b185 100755 --- a/FS/bin/freeside-daily +++ b/FS/bin/freeside-daily @@ -15,6 +15,9 @@ adminsuidsetup $user; use FS::Cron::bill qw(bill); bill(%opt); +use FS::Cron::notify qw(notify_flat_delay); +notify_flat_delay(%opt); + use FS::Cron::vacuum qw(vacuum); vacuum(); diff --git a/conf/impending_recur_template b/conf/impending_recur_template new file mode 100644 index 000000000..9075ac8bf --- /dev/null +++ b/conf/impending_recur_template @@ -0,0 +1,20 @@ + + +Ivan Kohler +12345 Test Lane +Truckee, CA 96161 + + +{ $first; } { $last; }: + + We thank you for your continuing patronage. This notice is to remind you +that your { $packages->[0] } Internet service will expire on { use Date::Format; time2str("%x", $recurdates->[0]); }. +At that time we will begin charging you on a recurring basis so that we may +continue your service uninterrupted. + +Very Truly Yours, + + SISD Service Team + + + -- 2.11.0