diff options
Diffstat (limited to 'FS')
| -rw-r--r-- | FS/FS.pm | 2 | ||||
| -rw-r--r-- | FS/FS/Conf.pm | 7 | ||||
| -rw-r--r-- | FS/FS/Cron/notify.pm | 134 | ||||
| -rw-r--r-- | FS/FS/Schema.pm | 12 | ||||
| -rw-r--r-- | FS/FS/cust_main.pm | 98 | ||||
| -rw-r--r-- | FS/FS/cust_pkg.pm | 14 | ||||
| -rw-r--r-- | FS/FS/cust_pkg_option.pm | 115 | ||||
| -rw-r--r-- | FS/FS/part_pkg/flat_delayed.pm | 9 | ||||
| -rwxr-xr-x | FS/bin/freeside-daily | 3 | 
9 files changed, 384 insertions, 10 deletions
| @@ -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 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 <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 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 .= <<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; 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<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 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<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; + 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(); | 
