notices before first charge on flat_delayed
authorjeff <jeff>
Tue, 23 Jan 2007 23:43:00 +0000 (23:43 +0000)
committerjeff <jeff>
Tue, 23 Jan 2007 23:43:00 +0000 (23:43 +0000)
FS/FS.pm
FS/FS/Conf.pm
FS/FS/Cron/notify.pm [new file with mode: 0644]
FS/FS/Schema.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/FS/cust_pkg_option.pm [new file with mode: 0644]
FS/FS/part_pkg/flat_delayed.pm
FS/bin/freeside-daily
conf/impending_recur_template [new file with mode: 0644]

index d8999ca..b18d7f7 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -111,6 +111,8 @@ L<FS::cust_svc> - Service class
 
 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
index bae6a77..3d1289d 100644 (file)
@@ -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 <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;
diff --git a/FS/FS/Cron/notify.pm b/FS/FS/Cron/notify.pm
new file mode 100644 (file)
index 0000000..579d50a
--- /dev/null
@@ -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 .= <<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 91af964..0d67834 100644 (file)
@@ -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',    '',   '', '', '', 
index ea52378..f6633f5 100644 (file)
@@ -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<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
index fcbb08c..c9b454c 100644 (file)
@@ -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 (file)
index 0000000..43a1530
--- /dev/null
@@ -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<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;
+
index ec11699..caade40 100644 (file)
@@ -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,
index b9742c4..a06a2b1 100755 (executable)
@@ -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 (file)
index 0000000..9075ac8
--- /dev/null
@@ -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
+
+
+