L<FS::cust_pkg> - Customer package class
+L<FS::cust_pkg_option> - Customer package option class
+
L<FS::cust_main> - Customer class
L<FS::cust_main_invoice> - Invoice destination
'type' => 'textarea',
},
+ {
+ 'key' => 'impending_recur_template',
+ 'section' => 'billing',
+ 'description' => 'Template file for alerts about looming first time recurrant billing. See the <a href="../docs/billing.html#invoice_template">billing documentation</a> for details. Also see packages with a <a href="../browse/part_pkg.cgi">flat price plan</a>',
+ 'type' => 'textarea',
+ },
+
);
1;
--- /dev/null
+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 .= <<END;
+ and 0 < ( select count(*) from cust_main
+ where cust_pkg.custnum = cust_main.custnum
+ and cust_main.agentnum = $opt{a}
+ )
+END
+ }
+
+ my @cust_pkg;
+ if ( @ARGV ) {
+ $where_pkg .= "and ( " . join( "OR ", map { "custnum = $_" } @ARGV) . " )";
+ }
+
+ my $orderby = "order by custnum, bill";
+
+ my $extra_sql = "$where_pkg $orderby";
+
+ @cust_pkg = qsearch('cust_pkg', {}, '', $extra_sql );
+
+ my @packages = ();
+ my @recurdates = ();
+ my @cust_pkgs = ();
+ while ( scalar(@cust_pkg) ) {
+ my $cust_main = $cust_pkg[0]->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;
'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', '', '', '', '',
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;
# 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";
}
+=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
+
+Sends a templated email notification to the customer (see L<Text::Template).
+
+OPTIONS is a hash and may include
+
+I<from> - the email sender (default is invoice_from)
+
+I<to> - comma-separated scalar or arrayref of recipients
+ (default is invoicing_list)
+
+I<subject> - The subject line of the sent email notification
+ (default is "Notice from company_name")
+
+I<extra_fields> - 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
# 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;
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;
}
- my $error = $new->SUPER::replace($old);
+ my $error = $new->SUPER::replace($old,
+ $options{options} ? ${options{options}} : ()
+ );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
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;
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;
$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;
--- /dev/null
+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<hash> 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<FS::Record>, L<FS::cust_pkg>, schema.html from the base documentation.
+
+=cut
+
+1;
+
'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,
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();
--- /dev/null
+
+
+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
+
+
+