import torrus 1.0.9
[freeside.git] / FS / FS / part_export / domreg_opensrs.pm
index ec73d3c..76f0059 100644 (file)
@@ -1,13 +1,13 @@
 package FS::part_export::domreg_opensrs;
 
-use vars qw(@ISA %info %options $conf);
+use vars qw(@ISA %info %options $conf $me $DEBUG);
 use Tie::IxHash;
+use DateTime;
 use FS::Record qw(qsearchs qsearch);
 use FS::Conf;
 use FS::part_export::null;
 use FS::svc_domain;
 use FS::part_pkg;
-use Net::OpenSRS;
 
 =head1 NAME
 
@@ -32,9 +32,15 @@ on the setting of the svc_domain's action field.
 
 =back
 
+This export uses Net::OpenSRS.  Registration and transfer attempts will fail unless Net::OpenSRS is installed
+and LWP::UserAgent is able to make HTTPS posts.  You can turn on debugging messages and use the OpenSRS test
+gateway when setting up this export.
+
 =cut
 
 @ISA = qw(FS::part_export::null);
+$me = '[' .  __PACKAGE__ . ']';
+$DEBUG = 0;
 
 my @tldlist = qw/com net org biz info name mobi at be ca cc ch cn de dk es eu fr it mx nl tv uk us/;
 
@@ -47,16 +53,20 @@ tie %options, 'Tie::IxHash',
                       },
   'masterdomain' => { label => 'Master domain at OpenSRS',
                       },
+  'wait_for_pay' => { label => 'Do not provision until payment is received',
+                      type => 'checkbox',
+                      default => '0',
+                    },
   'debug_level'  => { label => 'Net::OpenSRS debug level',
                       type => 'select',
                       options => [ 0, 1, 2, 3 ],
                      default => 0 },
-  'register'     => { label => 'Use for registration',
-                      type => 'checkbox',
-                      default => '1' },
-  'transfer'     => { label => 'Use for transfer',
-                      type => 'checkbox',
-                      default => '1' },
+#  'register'     => { label => 'Use for registration',
+#                      type => 'checkbox',
+#                      default => '1' },
+#  'transfer'     => { label => 'Use for transfer',
+#                      type => 'checkbox',
+#                      default => '1' },
   'tlds'         => { label => 'Use this export for these top-level domains (TLDs)',
                       type => 'select',
                       multi => 1,
@@ -123,6 +133,14 @@ sub format_tel {
   return $tel;
 }
 
+=item gen_contact_info
+
+Generates contact data for the domain based on the customer data.
+
+Currently relies on Net::OpenSRS to format the telephone number for OpenSRS.
+
+=cut
+
 sub gen_contact_info
 {
   my ($co)=@_;
@@ -154,19 +172,121 @@ sub gen_contact_info
   return $c;
 }
 
+=item validate_contact_info
+
+Attempts to validate contact data for the domain based on OpenSRS rules.
+
+Returns undef if the contact data is acceptable, an error message if the contact
+data lacks one or more required fields.
+
+=cut
+
+sub validate_contact_info {
+  my $c = shift;
+
+  my %fields = (
+    firstname => "first name",
+    lastname => "last name",
+    address => "street address",
+    city => "city", 
+    state => "state",
+    zip => "ZIP/postal code",
+    country => "country",
+    email => "email address",
+    phone => "phone number",
+  );
+  my @err = ();
+  foreach (keys %fields) {
+    if (!defined($c->{$_}) || !$c->{$_}) {
+      push @err, $fields{$_};
+    }
+  }
+  if (scalar(@err) > 0) {
+    return "Contact information needs: " . join(', ', @err);
+  }
+  undef;
+}
+
+=item testmode
+
+Returns the Net::OpenSRS-required test mode string based on whether the export
+is configured to use the live or the test gateway.
+
+=cut
+
 sub testmode {
   my $self = shift;
 
   return 'live' if $self->machine eq "rr-n1-tor.opensrs.net";
   return 'test' if $self->machine eq "horizon.opensrs.net";
   undef;
+
 }
 
+=item _export_insert
+
+Attempts to "export" the domain, i.e. register or transfer it if the user selected
+that option when editing the domain.
+
+Returns an error message on failure or undef on success.
+
+May also return an error message if it cannot load the required Perl module Net::OpenSRS,
+or if the domain is not registerable, or if insufficient data is provided in the customer
+record to generate the required contact information to register or transfer the domain.
+
+=cut
+
 sub _export_insert {
   my( $self, $svc_domain ) = ( shift, shift );
 
   return if $svc_domain->action eq 'I';  # Ignoring registration, just doing DNS
 
+  if ($svc_domain->action eq 'N') {
+    return $self->register( $svc_domain );
+  } elsif ($svc_domain->action eq 'M') {
+    return $self->transfer( $svc_domain );
+  } 
+  return "Unknown domain action " . $svc_domain->action;
+}
+
+sub _export_insert_on_payment {
+  my( $self, $svc_domain ) = ( shift, shift );
+  warn "$me:_export_insert_on_payment called\n" if $DEBUG;
+  return '' unless $self->option('wait_for_pay');
+
+  my $queue = new FS::queue {
+    'svcnum' => $svc_domain->svcnum,
+    'job'    => 'FS::part_export::domreg_opensrs::renew_through',
+  };
+  $queue->insert( $self, $svc_domain ); #_export_insert with 'R' action?
+}
+
+## Domain registration exports do nothing on replace.  Mainly because we haven't decided what they should do.
+#sub _export_replace {
+#  my( $self, $new, $old ) = (shift, shift, shift);
+#
+#  return '';
+#
+#}
+
+## Domain registration exports do nothing on delete.  You're just removing the domain from Freeside, not the registry
+#sub _export_delete {
+#  my( $self, $svc_domain ) = ( shift, shift );
+#
+#  return '';
+#}
+
+=item is_supported_domain
+
+Return undef if the domain name uses a TLD or SLD that is supported by this registrar.
+Otherwise return an error message explaining what's wrong.
+
+=cut
+
+sub is_supported_domain {
+  my $self = shift;
+  my $svc_domain = shift;
+
   # Get the TLD of the new domain
   my @bits = split /\./, $svc_domain->domain;
 
@@ -178,10 +298,15 @@ sub _export_insert {
   my @tlds = split /\s+/, $self->option('tlds');
   @tlds =  map { s/\.//; $_ } @tlds;
   return "Can't register top-level domain $tld, restricted to: " . $self->option('tlds') if ! grep { $_ eq $tld } @tlds;
+  return undef;
+}
 
-  my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main;
+=item get_srs
 
-  my $c = gen_contact_info($cust_main);
+=cut
+
+sub get_srs {
+  my $self = shift;
 
   my $srs = Net::OpenSRS->new();
 
@@ -191,40 +316,285 @@ sub _export_insert {
   $srs->set_key( $self->option('privatekey') );
 
   $srs->set_manage_auth( $self->option('username'), $self->option('password') );
+  return $srs;
+}
+
+=item get_status
+
+Returns a reference to a hashref containing information on the domain's status.  The keys
+defined depend on the status.
+
+'unregistered' means the domain is not registered.
+
+Otherwise, if the domain is in an asynchronous operation such as a transfer, returns the state
+of that operation.
+
+Otherwise returns a value indicating if the domain can be managed through our reseller account.
+
+=cut
+
+sub get_status {
+  my ( $self, $svc_domain ) = @_;
+  my $rslt = {};
+
+  eval "use Net::OpenSRS;";
+  return $@ if $@;
+
+  my $srs = $self->get_srs;
+
+  if ($srs->is_available( $svc_domain->domain )) {
+    $rslt->{'unregistered'} = 1;
+  } else {
+    $rslt = $srs->check_transfer( $svc_domain->domain );
+    if (defined($rslt->{'reason'})) {
+      my $rv = $srs->make_request(
+        {
+          action     => 'belongs_to_rsp',
+          object     => 'domain',
+          attributes => {
+            domain => $svc_domain->domain
+          }
+        }
+      );
+      if ($rv) {
+        $self->_set_response;
+        if ( $rv->{attributes}->{'domain_expdate'} ) {
+         $rslt->{'expdate'} = $rv->{attributes}->{'domain_expdate'};
+        }
+      }
+    }
+  }
+
+  return $rslt; # Success
+}
+
+=item register
+
+Attempts to register the domain through the reseller account associated with this export.
+
+Like most export functions, returns an error message on failure or undef on success.
+
+=cut
+
+sub register {
+  my ( $self, $svc_domain, $years ) = @_;
+
+  $years = 1 unless $years; #default to 1 year since we don't seem to pass it
+
+  return "Net::OpenSRS does not support period other than 1 year" if $years != 1;
+
+  eval "use Net::OpenSRS;";
+  return $@ if $@;
+
+  my $err = $self->is_supported_domain( $svc_domain );
+  return $err if $err;
+
+  my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main;
+
+  my $c = gen_contact_info($cust_main);
+
+  $err = validate_contact_info($c);
+  return $err if $err;
+
+  my $srs = $self->get_srs;
+
+#  cookie not required for registration
+#  my $cookie = $srs->get_cookie( $self->option('masterdomain') );
+#  if (!$cookie) {
+#     return "Unable to get cookie at OpenSRS: " . $srs->last_response();
+#  }
+
+#  return "Domain registration not enabled" if !$self->option('register');
+  return $srs->last_response() if !$srs->register_domain( $svc_domain->domain, $c);
+
+  return ''; # Should only get here if register succeeded
+}
+
+=item transfer
+
+Attempts to transfer the domain into the reseller account associated with this export.
+
+Like most export functions, returns an error message on failure or undef on success.
+
+=cut
+
+sub transfer {
+  my ( $self, $svc_domain ) = @_;
+
+  eval "use Net::OpenSRS;";
+  return $@ if $@;
+
+  my $err = $self->is_supported_domain( $svc_domain );
+  return $err if $err;
+
+  my $cust_main = $svc_domain->cust_svc->cust_pkg->cust_main;
+
+  my $c = gen_contact_info($cust_main);
+
+  $err = validate_contact_info($c);
+  return $err if $err;
+
+  my $srs = $self->get_srs;
 
   my $cookie = $srs->get_cookie( $self->option('masterdomain') );
   if (!$cookie) {
      return "Unable to get cookie at OpenSRS: " . $srs->last_response();
   }
 
-  if ($svc_domain->action eq 'N') {
-    return "Domain registration not enabled" if !$self->option('register');
-    return $srs->last_response() if !$srs->register_domain( $svc_domain->domain, $c);
-  } elsif ($svc_domain->action eq 'M') {
-    return "Domain transfer not enabled" if !$self->option('transfer');
-    return $srs->last_response() if !$srs->transfer_domain( $svc_domain->domain, $c);
-  } else {
-    return "Unknown domain action " . $svc_domain->action;
+#  return "Domain transfer not enabled" if !$self->option('transfer');
+  return $srs->last_response() if !$srs->transfer_domain( $svc_domain->domain, $c);
+
+  return ''; # Should only get here if transfer succeeded
+}
+
+=item renew
+
+Attempts to renew the domain for the specified number of years.
+
+Like most export functions, returns an error message on failure or undef on success.
+
+=cut
+
+sub renew {
+  my ( $self, $svc_domain, $years ) = @_;
+
+  eval "use Net::OpenSRS;";
+  return $@ if $@;
+
+  my $err = $self->is_supported_domain( $svc_domain );
+  return $err if $err;
+
+  my $srs = $self->get_srs;
+
+  my $cookie = $srs->get_cookie( $self->option('masterdomain') );
+  if (!$cookie) {
+     return "Unable to get cookie at OpenSRS: " . $srs->last_response();
   }
 
-  return ''; # Should only get here if register or transfer succeeded
+#  return "Domain renewal not enabled" if !$self->option('renew');
+  return $srs->last_response() if !$srs->renew_domain( $svc_domain->domain, $years );
 
+  return ''; # Should only get here if renewal succeeded
 }
 
-## Domain registration exports do nothing on replace.  Mainly because we haven't decided what they should do.
-#sub _export_replace {
-#  my( $self, $new, $old ) = (shift, shift, shift);
-#
-#  return '';
-#
-#}
+=item renew_through [ EPOCH_DATE ]
 
-## Domain registration exports do nothing on delete.  You're just removing the domain from Freeside, not the registry
-#sub _export_delete {
-#  my( $self, $svc_domain ) = ( shift, shift );
-#
-#  return '';
-#}
+Attempts to renew the domain through the specified date.  If no date is
+provided it is gleaned from the associated cust_pkg bill date
+
+Like some export functions, dies on failure or returns undef on success.
+It is always called from the queue.
+
+=cut
+
+sub renew_through {
+  my ( $self, $svc_domain, $date ) = @_;
+
+  warn "$me: renew_through called\n" if $DEBUG;
+  eval "use Net::OpenSRS;";
+  die $@ if $@;
+
+  unless ( $date ) {
+    my $cust_pkg = $svc_domain->cust_svc->cust_pkg;
+    die "Can't renew: no date specified and domain is not in a package."
+      unless $cust_pkg;
+    $date = $cust_pkg->bill;
+  }
+
+  my $err = $self->is_supported_domain( $svc_domain );
+  die $err if $err;
+
+  warn "$me: checking status\n" if $DEBUG;
+  my $rv = $self->get_status($svc_domain);
+  die "Domain ". $svc_domain->domain. " is not renewable"
+    unless $rv->{expdate};
+
+  die "Can't parse expiration date for ". $svc_domain->domain
+    unless $rv->{expdate} =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/;
+
+  my ($year,$month,$day,$hour,$minute,$second) = ($1,$2,$3,$4,$5,$6);
+  my $exp = DateTime->new( year   => $year,
+                           month  => $month,
+                           day    => $day,
+                           hour   => $hour,
+                           minute => $minute,
+                           second => $second,
+                           time_zone => 'America/New_York',#timezone of opensrs
+                         );
+
+  my $bill = DateTime->
+   from_epoch( 'epoch'     => $date,
+               'time_zone' => DateTime::TimeZone->new( name => 'local' ),
+  );
+
+  my $years = 0;
+  while ( DateTime->compare( $bill, $exp ) > 0 ) {
+    $years++;
+    $exp->add( 'years' => 1 );
+
+    die "Can't renew ". $svc_domain->domain. " for more than 10 years."
+      if $years > 10; #no infinite loop
+  }
+
+  return '' unless $years;
+
+  warn "$me: renewing ". $svc_domain->domain. " for $years years\n" if $DEBUG;
+  my $srs = $self->get_srs;
+  $rv = $srs->make_request(
+    {
+      action     => 'renew',
+      object     => 'domain',
+      attributes => {
+        domain                => $svc_domain->domain,
+        auto_renew            => 0,
+        handle                => 'process',
+        period                => $years,
+        currentexpirationyear => $year,
+      }
+    }
+  );
+  die $rv->{response_text} unless $rv->{is_success};
+
+  return ''; # Should only get here if renewal succeeded
+}
+
+=item revoke
+
+Attempts to revoke the domain registration.  Only succeeds if invoked during the OpenSRS
+grace period immediately after registration.
+
+Like most export functions, returns an error message on failure or undef on success.
+
+=cut
+
+sub revoke {
+  my ( $self, $svc_domain ) = @_;
+
+  eval "use Net::OpenSRS;";
+  return $@ if $@;
+
+  my $err = $self->is_supported_domain( $svc_domain );
+  return $err if $err;
+
+  my $srs = $self->get_srs;
+
+  my $cookie = $srs->get_cookie( $self->option('masterdomain') );
+  if (!$cookie) {
+     return "Unable to get cookie at OpenSRS: " . $srs->last_response();
+  }
+
+#  return "Domain registration revocation not enabled" if !$self->option('revoke');
+  return $srs->last_response() if !$srs->revoke_domain( $svc_domain->domain);
+
+  return ''; # Should only get here if transfer succeeded
+}
+
+=item registrar
+
+Should return a full-blown object representing OpenSRS, but current just returns a hashref
+containing the registrar name.
+
+=cut
 
 sub registrar {
   return {
@@ -236,9 +606,10 @@ sub registrar {
 
 =head1 SEE ALSO
 
-L<FS::part_export_option>, L<FS::export_svc>, L<FS::svc_domain>,
+L<Net::OpenSRS>, L<FS::part_export_option>, L<FS::export_svc>, L<FS::svc_domain>,
 L<FS::Record>, schema.html from the base documentation.
 
+
 =cut
 
 1;