event refactor, landing on HEAD!
[freeside.git] / FS / FS / Conf.pm
index 93ecf30..7f64058 100644 (file)
@@ -1,16 +1,18 @@
 package FS::Conf;
 
-use vars qw($base_dir @config_items @card_types $DEBUG );
+use vars qw($base_dir @config_items @base_items @card_types $DEBUG);
+use IO::File;
+use File::Basename;
 use MIME::Base64;
 use FS::ConfItem;
 use FS::ConfDefaults;
+use FS::Conf_compat17;
 use FS::conf;
 use FS::Record qw(qsearch qsearchs);
-use FS::UID qw(dbh);
+use FS::UID qw(dbh datasrc use_confcompat);
 
 $base_dir = '%%%FREESIDE_CONF%%%';
 
-
 $DEBUG = 0;
 
 =head1 NAME
@@ -72,51 +74,67 @@ sub base_dir {
   $1;
 }
 
-=item config KEY
+=item config KEY [ AGENTNUM ]
 
 Returns the configuration value or values (depending on context) for key.
+The optional agent number selects an agent specific value instead of the
+global default if one is present.
 
 =cut
 
+sub _usecompat {
+  my ($self, $method) = (shift, shift);
+  warn "NO CONFIGURATION RECORDS FOUND -- USING COMPATIBILITY MODE"
+    if use_confcompat;
+  my $compat = new FS::Conf_compat17 ("$base_dir/conf." . datasrc);
+  $compat->$method(@_);
+}
+
 sub _config {
-  my($self,$name,$agent)=@_;
+  my($self,$name,$agentnum)=@_;
   my $hashref = { 'name' => $name };
-  if (defined($agent) && $agent) {
-    $hashref->{agent} = $agent;
-  }
+  $hashref->{agentnum} = $agentnum;
   local $FS::Record::conf = undef;  # XXX evil hack prevents recursion
   my $cv = FS::Record::qsearchs('conf', $hashref);
-  if (!$cv && exists($hashref->{agent})) {
-    delete($hashref->{agent});
+  if (!$cv && defined($agentnum)) {
+    $hashref->{agentnum} = '';
     $cv = FS::Record::qsearchs('conf', $hashref);
   }
   return $cv;
 }
 
 sub config {
-  my($self,$name,$agent)=@_;
-  my $cv = $self->_config($name, $agent) or return;
+  my $self = shift;
+  return $self->_usecompat('config', @_) if use_confcompat;
+
+  my($name,$agentnum)=@_;
+  my $cv = $self->_config($name, $agentnum) or return;
 
   if ( wantarray ) {
-    split "\n", $cv->value;
+    my $v = $cv->value;
+    chomp $v;
+    (split "\n", $v, -1);
   } else {
     (split("\n", $cv->value))[0];
   }
 }
 
-=item config_binary KEY
+=item config_binary KEY [ AGENTNUM ]
 
 Returns the exact scalar value for key.
 
 =cut
 
 sub config_binary {
-  my($self,$name,$agent)=@_;
-  my $cv = $self->_config($name, $agent) or return;
+  my $self = shift;
+  return $self->_usecompat('config_binary', @_) if use_confcompat;
+
+  my($name,$agentnum)=@_;
+  my $cv = $self->_config($name, $agentnum) or return;
   decode_base64($cv->value);
 }
 
-=item exists KEY
+=item exists KEY [ AGENTNUM ]
 
 Returns true if the specified key exists, even if the corresponding value
 is undefined.
@@ -124,19 +142,28 @@ is undefined.
 =cut
 
 sub exists {
-  my($self,$name,$agent)=@_;
-  defined($self->_config($name, $agent));
-}
+  my $self = shift;
+  return $self->_usecompat('exists', @_) if use_confcompat;
 
-=item config_orbase KEY SUFFIX
+  my($name,$agentnum)=@_;
+  defined($self->_config($name, $agentnum));
+}
 
-Returns the configuration value or values (depending on context) for 
-KEY_SUFFIX, if it exists, otherwise for KEY
-
-=cut
+#=item config_orbase KEY SUFFIX
+#
+#Returns the configuration value or values (depending on context) for 
+#KEY_SUFFIX, if it exists, otherwise for KEY
+#
+#=cut
 
+# outmoded as soon as we shift to agentnum based config values
+# well, mostly.  still useful for e.g. late notices, etc. in that we want
+# these to fall back to standard values
 sub config_orbase {
-  my( $self, $name, $suffix ) = @_;
+  my $self = shift;
+  return $self->_usecompat('config_orbase', @_) if use_confcompat;
+
+  my( $name, $suffix ) = @_;
   if ( $self->exists("${name}_$suffix") ) {
     $self->config("${name}_$suffix");
   } else {
@@ -144,33 +171,65 @@ sub config_orbase {
   }
 }
 
-=item touch KEY
+=item invoice_templatenames
+
+Returns all possible invoice template names.
+
+=cut
+
+sub invoice_templatenames {
+  my( $self ) = @_;
+
+  my %templatenames = ();
+  foreach my $item ( $self->config_items ) {
+    foreach my $base ( @base_items ) {
+      my( $main, $ext) = split(/\./, $base);
+      $ext = ".$ext" if $ext;
+      if ( $item->key =~ /^${main}_(.+)$ext$/ ) {
+      $templatenames{$1}++;
+      }
+    }
+  }
+  
+  sort keys %templatenames;
+
+}
+
+=item touch KEY [ AGENT ];
 
 Creates the specified configuration key if it does not exist.
 
 =cut
 
 sub touch {
-  my($self, $name, $agent) = @_;
-  $self->set($name, '', $agent);
+  my $self = shift;
+  return $self->_usecompat('touch', @_) if use_confcompat;
+
+  my($name, $agentnum) = @_;
+  unless ( $self->exists($name, $agentnum) ) {
+    $self->set($name, '', $agentnum);
+  }
 }
 
-=item set KEY VALUE
+=item set KEY VALUE [ AGENTNUM ];
 
 Sets the specified configuration key to the given value.
 
 =cut
 
 sub set {
-  my($self, $name, $value, $agent) = @_;
+  my $self = shift;
+  return $self->_usecompat('set', @_) if use_confcompat;
+
+  my($name, $value, $agentnum) = @_;
   $value =~ /^(.*)$/s;
   $value = $1;
 
-  warn "[FS::Conf] SET $file\n" if $DEBUG;
+  warn "[FS::Conf] SET $name\n" if $DEBUG;
 
-  my $old = FS::Record::qsearchs('conf', {name => $name, agent => $agent});
+  my $old = FS::Record::qsearchs('conf', {name => $name, agentnum => $agentnum});
   my $new = new FS::conf { $old ? $old->hash 
-                                : ('name' => $name, 'agent' => $agent)
+                                : ('name' => $name, 'agentnum' => $agentnum)
                          };
   $new->value($value);
 
@@ -186,7 +245,7 @@ sub set {
 
 }
 
-=item set_binary KEY VALUE
+=item set_binary KEY VALUE [ AGENTNUM ]
 
 Sets the specified configuration key to an exact scalar value which
 can be retrieved with config_binary.
@@ -194,20 +253,26 @@ can be retrieved with config_binary.
 =cut
 
 sub set_binary {
-  my($self,$name, $value, $agent)=@_;
-  $self->set($name, encode_base64($value), $agent);
+  my $self  = shift;
+  return if use_confcompat;
+
+  my($name, $value, $agentnum)=@_;
+  $self->set($name, encode_base64($value), $agentnum);
 }
 
-=item delete KEY
+=item delete KEY [ AGENTNUM ];
 
 Deletes the specified configuration key.
 
 =cut
 
 sub delete {
-  my($self, $name, $agent) = @_;
-  if ( my $cv = FS::Record::qsearchs('conf', {name => $name, agent => $agent}) ) {
-    warn "[FS::Conf] DELETE $file\n";
+  my $self = shift;
+  return $self->_usecompat('delete', @_) if use_confcompat;
+
+  my($name, $agentnum) = @_;
+  if ( my $cv = FS::Record::qsearchs('conf', {name => $name, agentnum => $agentnum}) ) {
+    warn "[FS::Conf] DELETE $name\n";
 
     my $oldAutoCommit = $FS::UID::AutoCommit;
     local $FS::UID::AutoCommit = 0;
@@ -225,81 +290,192 @@ sub delete {
   }
 }
 
+=item import_config_item CONFITEM DIR 
+
+  Imports the item specified by the CONFITEM (see L<FS::ConfItem>) into
+the database as a conf record (see L<FS::conf>).  Imports from the file
+in the directory DIR.
+
+=cut
+
+sub import_config_item { 
+  my ($self,$item,$dir) = @_;
+  my $key = $item->key;
+  if ( -e "$dir/$key" && ! use_confcompat ) {
+    warn "Inserting $key\n" if $DEBUG;
+    local $/;
+    my $value = readline(new IO::File "$dir/$key");
+    if ($item->type eq 'binary') {
+      $self->set_binary($key, $value);
+    }else{
+      $self->set($key, $value);
+    }
+  }else {
+    warn "Not inserting $key\n" if $DEBUG;
+  }
+}
+
+=item verify_config_item CONFITEM DIR 
+
+  Compares the item specified by the CONFITEM (see L<FS::ConfItem>) in
+the database to the legacy file value in DIR.
+
+=cut
+
+sub verify_config_item { 
+  return '' if use_confcompat;
+  my ($self,$item,$dir) = @_;
+  my $key = $item->key;
+  my $type = $item->type;
+
+  my $compat = new FS::Conf_compat17 $dir;
+  my $error = '';
+  
+  $error .= "$key fails existential comparison; "
+    if $self->exists($key) xor $compat->exists($key);
+
+  unless ($type eq 'binary') {
+    {
+      no warnings;
+      $error .= "$key fails scalar comparison; "
+        unless scalar($self->config($key)) eq scalar($compat->config($key));
+    }
+
+    my (@new) = $self->config($key);
+    my (@old) = $compat->config($key);
+    unless ( scalar(@new) == scalar(@old)) { 
+      $error .= "$key fails list comparison; ";
+    }else{
+      my $r=1;
+      foreach (@old) { $r=0 if ($_ cmp shift(@new)); }
+      $error .= "$key fails list comparison; "
+        unless $r;
+    }
+  }
+
+  if ($type eq 'binary') {
+    $error .= "$key fails binary comparison; "
+      unless scalar($self->config_binary($key)) eq scalar($compat->config_binary($key));
+  }
+
+  if ($error =~ /existential comparison/ && $item->section eq 'deprecated') {
+    my $proto;
+    for ( @config_items ) { $proto = $_; last if $proto->key eq $key;  }
+    unless ($proto->key eq $key) { 
+      warn "removed config item $error\n" if $DEBUG;
+      $error = '';
+    }
+  }
+
+  $error;
+}
+
+#item _orbase_items OPTIONS
+#
+#Returns all of the possible extensible config items as FS::ConfItem objects.
+#See #L<FS::ConfItem>.  OPTIONS consists of name value pairs.  Possible
+#options include
+#
+# dir - the directory to search for configuration option files instead
+#       of using the conf records in the database
+#
+#cut
+
+#quelle kludge
+sub _orbase_items {
+  my ($self, %opt) = @_; 
+
+  my $listmaker = sub { my $v = shift;
+                        $v =~ s/_/!_/g;
+                        if ( $v =~ /\.(png|eps)$/ ) {
+                          $v =~ s/\./!_%./;
+                        }else{
+                          $v .= '!_%';
+                        }
+                        map { $_->name }
+                          FS::Record::qsearch( 'conf',
+                                               {},
+                                               '',
+                                               "WHERE name LIKE '$v' ESCAPE '!'"
+                                             );
+                      };
+
+  if (exists($opt{dir}) && $opt{dir}) {
+    $listmaker = sub { my $v = shift;
+                       if ( $v =~ /\.(png|eps)$/ ) {
+                         $v =~ s/\./_*./;
+                       }else{
+                         $v .= '_*';
+                       }
+                       map { basename $_ } glob($opt{dir}. "/$v" );
+                     };
+  }
+
+  ( map { 
+          my $proto;
+          my $base = $_;
+          for ( @config_items ) { $proto = $_; last if $proto->key eq $base;  }
+          die "don't know about $base items" unless $proto->key eq $base;
+
+          map { new FS::ConfItem { 
+                                   'key' => $_,
+                                   'section' => $proto->section,
+                                   'description' => 'Alternate ' . $proto->description . '  See the <a href="../docs/billing.html">billing documentation</a> for details.',
+                                   'type' => $proto->type,
+                                 };
+              } &$listmaker($base);
+        } @base_items,
+  );
+}
+
 =item config_items
 
-Returns all of the possible configuration items as FS::ConfItem objects.  See
-L<FS::ConfItem>.
+Returns all of the possible global/default configuration items as
+FS::ConfItem objects.  See L<FS::ConfItem>.
 
 =cut
 
 sub config_items {
   my $self = shift; 
-  #quelle kludge
-  @config_items,
-  ( map { 
-        new FS::ConfItem {
-                           'key'         => $_->name,
-                           'section'     => 'billing',
-                           'description' => 'Alternate template file for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
-                           'type'        => 'textarea',
-                         }
-      } FS::Record::qsearch('conf', {}, '', "WHERE name LIKE 'invoice!_template!_%' ESCAPE '!'")
-  ),
-  ( map { 
-        new FS::ConfItem {
-                           'key'         => '$_->name',
-                           'section'     => 'billing',  #? 
-                           'description' => 'An image to include in some types of invoices',
-                           'type'        => 'binary',
-                         }
-      } FS::Record::qsearch('conf', {}, '', "WHERE name LIKE 'logo!_%.png' ESCAPE '!'")
-  ),
-  ( map { 
-        new FS::ConfItem {
-                           'key'         => $_->name,
-                           'section'     => 'billing',
-                           'description' => 'Alternate HTML template for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
-                           'type'        => 'textarea',
-                         }
-      } FS::Record::qsearch('conf', {}, '', "WHERE name LIKE 'invoice!_html!_%' ESCAPE '!'")
-  ),
-  ( map { 
-        ($latexname = $_->name ) =~ s/latex/html/;
-        new FS::ConfItem {
-                           'key'         => $_->name,
-                           'section'     => 'billing',
-                           'description' => "Alternate Notes section for HTML invoices.  Defaults to the same data in $latexname if not specified.",
-                           'type'        => 'textarea',
-                         }
-      } FS::Record::qsearch('conf', {}, '', "WHERE name LIKE 'invoice!_htmlnotes!_%' ESCAPE '!'")
-  ),
-  ( map { 
-        new FS::ConfItem {
-                           'key'         => $_->name,
-                           'section'     => 'billing',
-                           'description' => 'Alternate LaTeX template for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
-                           'type'        => 'textarea',
-                         }
-      } FS::Record::qsearch('conf', {}, '', "WHERE name LIKE 'invoice!_latex!_%' ESCAPE '!'")
-  ),
-  ( map { 
-        new FS::ConfItem {
-                           'key'         => '$_->name',
-                           'section'     => 'billing',  #? 
-                           'description' => 'An image to include in some types of invoices',
-                           'type'        => 'binary',
-                         }
-      } FS::Record::qsearch('conf', {}, '', "WHERE name LIKE 'logo!_%.eps' ESCAPE '!'")
-  ),
-  ( map { 
-        new FS::ConfItem {
-                           'key'         => $_->name,
-                           'section'     => 'billing',
-                           'description' => 'Alternate Notes section for LaTeX typeset PostScript invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
-                           'type'        => 'textarea',
-                         }
-      } FS::Record::qsearch('conf', {}, '', "WHERE name LIKE 'invoice!_latexnotes!_%' ESCAPE '!'")
-  );
+  return $self->_usecompat('config_items', @_) if use_confcompat;
+
+  ( @config_items, $self->_orbase_items(@_) );
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item init-config DIR
+
+Imports the non-deprecated configuration items from DIR (1.7 compatible)
+to conf records in the database.
+
+=cut
+
+sub init_config {
+  my $dir = shift;
+
+  {
+    local $FS::UID::use_confcompat = 0;
+    my $conf = new FS::Conf;
+    foreach my $item ( $conf->config_items(dir => $dir) ) {
+      $conf->import_config_item($item, $dir);
+      my $error = $conf->verify_config_item($item, $dir);
+      return $error if $error;
+    }
+  
+    my $compat = new FS::Conf_compat17 $dir;
+    foreach my $item ( $compat->config_items ) {
+      my $error = $conf->verify_config_item($item, $dir);
+      return $error if $error;
+    }
+  }
+
+  $FS::UID::use_confcompat = 0;
+  '';  #success
 }
 
 =back
@@ -331,6 +507,36 @@ httemplate/docs/config.html
   "Solo",
 );
 
+@base_items = qw (
+                   invoice_template
+                   invoice_latex
+                   invoice_latexreturnaddress
+                   invoice_latexfooter
+                   invoice_latexsmallfooter
+                   invoice_latexnotes
+                   invoice_html
+                   invoice_htmlreturnaddress
+                   invoice_htmlfooter
+                   invoice_htmlnotes
+                   logo.png
+                   logo.eps
+                 );
+
+@base_items = qw (
+                   invoice_template
+                   invoice_latex
+                   invoice_latexreturnaddress
+                   invoice_latexfooter
+                   invoice_latexsmallfooter
+                   invoice_latexnotes
+                   invoice_html
+                   invoice_htmlreturnaddress
+                   invoice_htmlfooter
+                   invoice_htmlnotes
+                   logo.png
+                   logo.eps
+                 );
+
 @config_items = map { new FS::ConfItem $_ } (
 
   {
@@ -649,6 +855,20 @@ httemplate/docs/config.html
   },
 
   {
+    'key'         => 'lpr-postscript_prefix',
+    'section'     => 'billing',
+    'description' => 'Raw printer commands prepended to the beginning of postscript print jobs (evaluated as a double-quoted perl string - backslash escapes are available)',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'lpr-postscript_suffix',
+    'section'     => 'billing',
+    'description' => 'Raw printer commands added to the end of postscript print jobs (evaluated as a double-quoted perl string - backslash escapes are available)',
+    'type'        => 'text',
+  },
+
+  {
     'key'         => 'money_char',
     'section'     => '',
     'description' => 'Currency symbol - defaults to `$\'',
@@ -911,6 +1131,13 @@ httemplate/docs/config.html
     'type'        => 'checkbox',
   },
 
+  {
+    'key'         => 'show_bankstate',
+    'section'     => 'UI',
+    'description' => "Turns on display/collection of state for bank accounts in the web interface.  Sometimes required by electronic check (ACH) processors.",
+    'type'        => 'checkbox',
+  },
+
   { 
     'key'         => 'agent_defaultpkg',
     'section'     => 'UI',
@@ -1120,7 +1347,7 @@ httemplate/docs/config.html
   {
     'key'         => 'emailcancel',
     'section'     => 'billing',
-    'description' => 'Enable emailing of cancellation notices.',
+    'description' => 'Enable emailing of cancellation notices.  Make sure to fill in the cancelmessage and cancelsubject configuration values as well.',
     'type'        => 'checkbox',
   },
 
@@ -1150,6 +1377,7 @@ httemplate/docs/config.html
     'section'     => '',
     'description' => 'Template file for welcome email.  Welcome emails are sent to the customer email invoice destination(s) each time a svc_acct record is created.  See the <a href="http://search.cpan.org/~mjd/Text-Template/lib/Text/Template.pm">Text::Template</a> documentation for details on the template substitution language.  The following variables are available<ul><li><code>$username</code> <li><code>$password</code> <li><code>$first</code> <li><code>$last</code> <li><code>$pkg</code></ul>',
     'type'        => 'textarea',
+    'per_agent'   => 1,
   },
 
   {
@@ -1157,6 +1385,7 @@ httemplate/docs/config.html
     'section'     => '',
     'description' => 'From: address header for welcome email',
     'type'        => 'text',
+    'per_agent'   => 1,
   },
 
   {
@@ -1164,6 +1393,7 @@ httemplate/docs/config.html
     'section'     => '',
     'description' => 'Subject: header for welcome email',
     'type'        => 'text',
+    'per_agent'   => 1,
   },
   
   {
@@ -1172,6 +1402,14 @@ httemplate/docs/config.html
     'description' => 'MIME type for welcome email',
     'type'        => 'select',
     'select_enum' => [ 'text/plain', 'text/html' ],
+    'per_agent'   => 1,
+  },
+
+  {
+    'key'         => 'welcome_letter',
+    'section'     => '',
+    'description' => 'Optional LaTex template file for a printed welcome letter.  A welcome letter is printed the first time a cust_pkg record is created.  See the <a href="http://search.cpan.org/~mjd/Text-Template/lib/Text/Template.pm">Text::Template</a> documentation and the billing documentation for details on the template substitution language.  A variable exists for each fieldname in the customer record (<code>$first, $last, etc</code>).  The following additional variables are available<ul><li><code>$payby</code> - a friendler represenation of the field<li><code>$payinfo</code> - the masked payment information<li><code>$expdate</code> - the time at which the payment method expires (a UNIX timestamp)<li><code>$returnaddress</code> - the invoice return address for this customer\'s agent</ul>',
+    'type'        => 'textarea',
   },
 
   {
@@ -1713,6 +1951,25 @@ httemplate/docs/config.html
   },
 
   {
+    'key'         => 'disable-fuzzy',
+    'section'     => 'UI',
+    'description' => 'Disable fuzzy searching.  Speeds up searching for large sites, but only shows exact matches.',
+    'type'        => 'checkbox',
+  },
+
+  { 'key'         => 'pkg_referral',
+    'section'     => '',
+    'description' => 'Enable package-specific advertising sources.',
+    'type'        => 'checkbox',
+  },
+
+  { 'key'         => 'pkg_referral-multiple',
+    'section'     => '',
+    'description' => 'In addition, allow multiple advertising sources to be associated with a single package.',
+    'type'        => 'checkbox',
+  },
+
+  {
     'key'         => 'dashboard-toplist',
     'section'     => 'UI',
     'description' => 'List of items to display on the top of the front page',
@@ -1776,7 +2033,20 @@ httemplate/docs/config.html
     'type'        => 'text',
   },
 
+  {
+    'key'         => 'disable_void_after',
+    'section'     => 'billing',
+    'description' => 'Number of seconds after which freeside won\'t attempt to VOID a payment first when performing a refund.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'disable_line_item_date_ranges',
+    'section'     => 'billing',
+    'description' => 'Prevent freeside from automatically generating date ranges on invoice line items.',
+    'type'        => 'checkbox',
+  },
+
 );
 
 1;
-