From 15e57a4859d967a13113602b112c4aa197ca6002 Mon Sep 17 00:00:00 2001 From: jeff Date: Wed, 27 May 2009 07:50:40 +0000 Subject: [PATCH] bulk provisioning via ftp and SOAP #5202 --- FS/FS/ClientAPI/Bulk.pm | 384 +++++++++++++++++++++ FS/FS/Conf.pm | 18 + FS/FS/Schema.pm | 26 ++ FS/FS/cust_main.pm | 13 +- FS/FS/cust_pkg.pm | 32 ++ FS/FS/cust_recon.pm | 193 +++++++++++ FS/FS/part_pkg/voip_cdr.pm | 2 +- FS/FS/svc_acct.pm | 15 + FS/MANIFEST | 2 + FS/bin/freeside-selfservice-server | 23 ++ FS/t/cust_recon.t | 5 + fs_selfservice/FS-SelfService/MANIFEST | 1 + fs_selfservice/FS-SelfService/Makefile.PL | 1 + fs_selfservice/FS-SelfService/SelfService.pm | 2 + .../FS-SelfService/freeside-selfservice-clientd | 149 ++++++-- .../freeside-selfservice-soap-server | 53 +++ .../FS-SelfService/iZoomOnlineProvisionService.pm | 75 ++++ 17 files changed, 970 insertions(+), 24 deletions(-) create mode 100644 FS/FS/ClientAPI/Bulk.pm create mode 100644 FS/FS/cust_recon.pm create mode 100644 FS/t/cust_recon.t create mode 100644 fs_selfservice/FS-SelfService/freeside-selfservice-soap-server create mode 100644 fs_selfservice/FS-SelfService/iZoomOnlineProvisionService.pm diff --git a/FS/FS/ClientAPI/Bulk.pm b/FS/FS/ClientAPI/Bulk.pm new file mode 100644 index 000000000..ec617df76 --- /dev/null +++ b/FS/FS/ClientAPI/Bulk.pm @@ -0,0 +1,384 @@ +package FS::ClientAPI::Bulk; + +use strict; + +use vars qw( $DEBUG $cache ); +use Date::Parse; +use FS::Record qw( qsearchs ); +use FS::Conf; +use FS::ClientAPI_SessionCache; +use FS::cust_main; +use FS::cust_pkg; +use FS::cust_svc; +use FS::svc_acct; +use FS::svc_external; +use FS::cust_recon; +use Data::Dumper; + +$DEBUG = 1; + +sub _cache { + $cache ||= new FS::ClientAPI_SessionCache ( { + 'namespace' => 'FS::ClientAPI::Agent', #yes, share session_ids + } ); +} + +sub _izoom_ftp_row_fixup { + my $hash = shift; + + my @addr_fields = qw( address1 address2 city state zip ); + my @fields = ( qw( agent_custid username _password first last ), + @addr_fields, + map { "ship_$_" } @addr_fields ); + + $hash->{$_} =~ s/[&\/\*'"]/_/g foreach @fields; + + #$hash->{action} = '' if $hash->{action} eq 'R'; #unsupported for ftp + + $hash->{refnum} = 1; #ahem + $hash->{country} = 'US'; + $hash->{ship_country} = 'US'; + $hash->{payby} = 'LECB'; + $hash->{payinfo} = $hash->{daytime}; + $hash->{ship_fax} = '' if ( !$hash->{sms} || $hash->{sms} eq 'F' ); + + my $has_ship = + grep { $hash->{"ship_$_"} && + (! $hash->{$_} || $hash->{"ship_$_"} ne $hash->{$_} ) + } + ( @addr_fields, 'fax' ); + + if ( $has_ship ) { + foreach ( @addr_fields, qw( first last ) ) { + $hash->{"ship_$_"} = $hash->{$_} unless $hash->{"ship_$_"}; + } + } + + delete $hash->{sms}; + + ''; + +}; + +sub _izoom_ftp_result { + my ($hash, $error) = @_; + my $cust_main = + qsearchs( 'cust_main', { 'agent_custid' => $hash->{agent_custid}, + 'agentnum' => $hash->{agentnum} + } + ); + + my $custnum = $cust_main ? $cust_main->custnum : ''; + my @response = ( $hash->{action}, $hash->{agent_custid}, $custnum ); + + if ( $error ) { + push @response, ( 'ERROR', $error ); + } else { + push @response, ( 'OK', 'OK' ); + } + + join( ',', @response ); + +} + +sub _izoom_ftp_badaction { + "Invalid action: $_[0] record: @_ "; +} + +sub _izoom_soap_row_fixup { _izoom_ftp_row_fixup(@_) }; + +sub _izoom_soap_result { + my ($hash, $error) = @_; + + if ( $hash->{action} eq 'R' ) { + if ( $error ) { + return "Please check errors:\n $error"; # odd extra space + } else { + return join(' ', "Everything ok.", $hash->{pkg}, $hash->{adjourn} ); + } + } + + my $pkg = $hash->{pkg} || $hash->{saved_pkg} || ''; + if ( $error ) { + return join(' ', $hash->{agent_custid}, $error ); + } else { + return join(' ', $hash->{agent_custid}, $pkg, $hash->{adjourn} ); + } + +} + +sub _izoom_soap_badaction { + "Unknown action '$_[13]' "; +} + +my %format = ( + 'izoom-ftp' => { + 'fields' => [ qw ( action agent_custid username _password + daytime ship_fax sms first last + address1 address2 city state zip + pkg adjourn ship_address1 ship_address2 + ship_city ship_state ship_zip ) ], + 'fixup' => sub { _izoom_ftp_row_fixup(@_) }, + 'result' => sub { _izoom_ftp_result(@_) }, + 'action' => sub { _izoom_ftp_badaction(@_) }, + }, + 'izoom-soap' => { + 'fields' => [ qw ( agent_custid username _password + daytime first last address1 address2 + city state zip pkg action adjourn + ship_fax sms ship_address1 ship_address2 + ship_city ship_state ship_zip ) ], + 'fixup' => sub { _izoom_soap_row_fixup(@_) }, + 'result' => sub { _izoom_soap_result(@_) }, + 'action' => sub { _izoom_soap_badaction(@_) }, + }, +); + +sub processrow { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $conf = new FS::Conf; + my $format = $conf->config('selfservice-bulk_format', $session->{agentnum}) + || 'izoom-soap'; + my ( @row ) = @{ $p->{row} }; + + warn "processrow called with '". join("' '", @row). "'\n" if $DEBUG; + + return { 'error' => "unknown format: $format" } + unless exists $format{$format}; + + return { 'error' => "Invalid record record length: ". scalar(@row). + "record: @row " #sic + } + unless scalar(@row) == scalar(@{$format{$format}{fields}}); + + my %hash = ( 'agentnum' => $session->{agentnum} ); + my $error; + + foreach my $field ( @{ $format{ $format }{ fields } } ) { + $hash{$field} = shift @row; + } + + $error ||= &{ $format{ $format }{ fixup } }( \%hash ); + + # put in the fixup routine? + if ( 'R' eq $hash{action} ) { + warn "processing reconciliation\n" if $DEBUG; + $error ||= process_recon($hash{agentnum}, $hash{agent_custid}); + } elsif ( 'P' eq $hash{action} ) { + # do nothing + } elsif( 'D' eq $hash{action} ) { + $hash{promo_pkg} = 'disk-1-'. $session->{agent}; + } elsif ( 'S' eq $hash{action} ) { + $hash{promo_pkg} = 'disk-2-'. $session->{agent}; + $hash{saved_pkg} = $hash{pkg}; + $hash{pkg} = ''; + } else { + $error ||= &{ $format{ $format }{ action } }( @row ); + } + + warn "processing provision\n" if ($DEBUG && !$error && $hash{action} ne 'R'); + $error ||= provision( %hash ) unless $hash{action} eq 'R'; + + my $result = &{ $format{ $format }{ result } }( \%hash, $error ); + + warn "processrow returning '". join("' '", $result, $error). "'\n" + if $DEBUG; + + return { 'error' => $error, 'message' => $result }; + +} + +sub provision { + my %args = ( @_ ); + + delete $args{action}; + + my $cust_main = + qsearchs( 'cust_main', + { map { $_ => $args{$_} } qw ( agent_custid agentnum ) }, + ); + + unless ( $cust_main ) { + $cust_main = new FS::cust_main { %args }; + my $error = $cust_main->insert; + return $error if $error; + } + + my @pkgs = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; + if ( scalar(@pkgs) > 1 ) { + return "Invalid account, should not be more then one active package ". #sic + "but found: ". scalar(@pkgs). " packages."; + } + + my $part_pkg = qsearchs( 'part_pkg', { 'pkg' => $args{pkg} } ) + or return "Unknown pkgpart: $args{pkg}" + if $args{pkg}; + + + my $create_package = $args{pkg}; + if ( scalar(@pkgs) && $create_package ) { + my $pkg = pop(@pkgs); + + if ( $part_pkg->pkgpart != $pkg->pkgpart ) { + my @cust_bill_pkg = $pkg->cust_bill_pkg(); + if ( 1 == scalar(@cust_bill_pkg) ) { + my $cbp= pop(@cust_bill_pkg); + my $cust_bill = $cbp->cust_bill; + $cust_bill->delete(); #really? wouldn't a credit be better? + } + $pkg->cancel(); + } else { + $create_package = ''; + $pkg->setfield('adjourn', str2time($args{adjourn})); + my $error = $pkg->replace(); + return $error if $error; + } + } + + if ( $create_package ) { + my $cust_pkg = new FS::cust_pkg ( { + 'pkgpart' => $part_pkg->pkgpart, + 'adjourn' => str2time( $args{adjourn} ), + } ); + + my $svcpart = $part_pkg->svcpart('svc_acct'); + + my $svc_acct = new FS::svc_acct ( { + 'svcpart' => $svcpart, + 'username' => $args{username}, + '_password' => $args{_password}, + } ); + + my $error = $cust_main->order_pkg( cust_pkg => $cust_pkg, + svcs => [ $svc_acct ], + ); + return $error if $error; + } + + if ( $args{promo_pkg} ) { + my $part_pkg = + qsearchs( 'part_pkg', { 'promo_code' => $args{promo_pkg} } ) + or return "unknown pkgpart: $args{promo_pkg}"; + + my $svcpart = $part_pkg->svcpart('svc_external') + or return "unknown svcpart: svc_external"; + + my $cust_pkg = new FS::cust_pkg ( { + 'svcpart' => $svcpart, + 'pkgpart' => $part_pkg->pkgpart, + } ); + + my $svc_ext = new FS::svc_external ( { 'svcpart' => $svcpart } ); + + my $ticket_subject = 'Send setup disk to customer '. $cust_main->custnum; + my $error = $cust_main->order_pkg ( cust_pkg => $cust_pkg, + svcs => [ $svc_ext ], + noexport => 1, + ticket_subject => $ticket_subject, + ticket_queue => "disk-$args{agentnum}", + ); + return $error if $error; + } + + my $error = $cust_main->bill(); + return $error if $error; +} + +sub process_recon { + my ( $agentnum, $id ) = @_; + my @recs = split /;/, $id; + my $err = ''; + foreach my $rec ( @recs ) { + my @record = split /,/, $rec; + my $result = process_recon_record(@record, $agentnum); + $err .= "$result\n" if $result; + } + return $err; +} + +sub process_recon_record { + my ( $agent_custid, $username, $_password, $daytime, $first, $last, $address1, $address2, $city, $state, $zip, $pkg, $adjourn, $agentnum) = @_; + + warn "process_recon_record called with '". join("','", @_). "'\n" if $DEBUG; + + my ($cust_pkg, $package); + + my $cust_main = + qsearchs( 'cust_main', + { 'agent_custid' => $agent_custid, 'agentnum' => $agentnum }, + ); + + my $comments = ''; + if ( $cust_main ) { + my @cust_pkg = grep { $_->part_pkg->freq } $cust_main->ncancelled_pkgs; + if ( scalar(@cust_pkg) == 1) { + $cust_pkg = pop(@cust_pkg); + $package = $cust_pkg->part_pkg->pkg; + $comments = "$agent_custid wrong package, expected: $pkg found: $package" + if ( $pkg ne $package ); + } else { + $comments = "invalid account, should be one active package but found: ". + scalar(@cust_pkg). " packages."; + } + } else { + $comments = + "Customer not found agent_custid=$agent_custid, agentnum=$agentnum"; + } + + my $cust_recon = new FS::cust_recon( { + 'recondate' => time, + 'agentnum' => $agentnum, + 'first' => $first, + 'last' => $last, + 'address1' => $address1, + 'address2' => $address2, + 'city' => $city, + 'state' => $state, + 'zip' => $zip, + 'custnum' => $cust_main ? $cust_main->custnum : '', #really? + 'status' => $cust_main ? $cust_main->status : '', + 'pkg' => $package, + 'adjourn' => $cust_pkg ? $cust_pkg->adjourn : '', + 'agent_custid' => $agent_custid, # redundant? + 'agent_pkg' => $pkg, + 'agent_adjourn' => str2time($adjourn), + 'comments' => $comments, + } ); + + warn Dumper($cust_recon) if $DEBUG; + my $error = $cust_recon->insert; + return $error if $error; + + warn "process_recon_record returning $comments\n" if $DEBUG; + + $comments; + +} + +sub check_username { + my $p = shift; + + my $session = _cache->get($p->{'session_id'}) + or return { 'error' => "Can't resume session" }; #better error message + + my $svc_domain = qsearchs( 'svc_domain', { 'domain' => $p->{domain} } ) + or return { 'error' => 'Unknown domain '. $p->{domain} }; + + my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{user}, + 'domsvc' => $svc_domain->svcnum, + }, + ); + + return { 'error' => $p->{user}. '@'. $p->{domain}. " alerady in use" } # sic + if $svc_acct; + + return { 'error' => '', + 'message' => $p->{user}. '@'. $p->{domain}. " is free" + }; +} + +1; diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 8b27610a7..cb53ef7d1 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -1153,6 +1153,7 @@ worry that config_items is freeside-specific and icky. 'section' => 'username', 'description' => 'Usernames must contain at least one letter', 'type' => 'checkbox', + 'per_agent' => 1, }, { @@ -2670,6 +2671,23 @@ worry that config_items is freeside-specific and icky. }, { + 'key' => 'selfservice-bulk_format', + 'section' => '', + 'description' => 'Parameter arrangement for selfservice bulk features', + 'type' => 'select', + 'select_enum' => [ '', 'izoom-soap', 'izoom-ftp' ], + 'per_agent' => 1, + }, + + { + 'key' => 'selfservice-bulk_ftp_dir', + 'section' => '', + 'description' => 'Enable bulk ftp provisioning in this folder', + 'type' => 'text', + 'per_agent' => 1, + }, + + { 'key' => 'signup-no_company', 'section' => '', 'description' => "Don't display a field for company name on signup.", diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index e9861f8d1..238058374 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -716,6 +716,32 @@ sub tables_hashref { ], }, + 'cust_recon' => { # what purpose does this serve? + 'columns' => [ + 'reconid', 'serial', '', '', '', '', + 'recondate', @date_type, '', '', + 'custnum', 'int' , '', '', '', '', + 'agentnum', 'int', '', '', '', '', + 'last', 'varchar', '', $char_d, '', '', + 'first', 'varchar', '', $char_d, '', '', + 'address1', 'varchar', '', $char_d, '', '', + 'address2', 'varchar', 'NULL', $char_d, '', '', + 'city', 'varchar', '', $char_d, '', '', + 'state', 'varchar', 'NULL', $char_d, '', '', + 'zip', 'varchar', 'NULL', 10, '', '', + 'pkg', 'varchar', 'NULL', $char_d, '', '', + 'adjourn', @date_type, '', '', + 'status', 'varchar', 'NULL', 10, '', '', + 'agent_custid', 'varchar', '', $char_d, '', '', + 'agent_pkg', 'varchar', 'NULL', $char_d, '', '', + 'agent_adjourn', @date_type, '', '', + 'comments', 'text', 'NULL', '', '', '', + ], + 'primary_key' => 'reconid', + 'unique' => [], + 'index' => [], + }, + #eventually use for billing & ship from cust_main too #for now, just cust_pkg locations 'cust_location' => { diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index a1bb926b3..72b84504f 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -709,6 +709,14 @@ jobs will have a dependancy on the supplied job (they will not run until the specific job completes). This can be used to defer provisioning until some action completes (such as running the customer's credit card successfully). +=item ticket_subject + +Optional subject for a ticket created and attached to this customer + +=item ticket_subject + +Optional queue name for ticket additions + =back =cut @@ -728,6 +736,9 @@ sub order_pkg { $svc_options{'depend_jobnum'} = $opt->{'depend_jobnum'} if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'}; + my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () } + qw( ticket_subject ticket_queue ); + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -751,7 +762,7 @@ sub order_pkg { $cust_pkg->custnum( $self->custnum ); - my $error = $cust_pkg->insert; + my $error = $cust_pkg->insert( %insert_params ); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "inserting cust_pkg (transaction rolled back): $error"; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 93aec6d38..0e5f3b7ca 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -6,6 +6,7 @@ use Carp qw(cluck); use Scalar::Util qw( blessed ); use List::Util qw(max); use Tie::IxHash; +use MIME::Entity; use FS::UID qw( getotaker dbh ); use FS::Misc qw( send_email ); use FS::Record qw( qsearch qsearchs ); @@ -229,6 +230,14 @@ If set true, supresses any referral credit to a referring customer. cust_pkg_option records will be created +=item ticket_subject + +a ticket will be added to this customer with this subject + +=item ticket_queue + +an optional queue name for ticket additions + =back =cut @@ -271,6 +280,29 @@ sub insert { my $conf = new FS::Conf; + if ( $conf->config('ticket_system') && $options{ticket_subject} ) { + eval ' + use lib ( "/opt/rt3/local/lib", "/opt/rt3/lib" ); + use RT; + '; + die $@ if $@; + + RT::LoadConfig(); + RT::Init(); + my $q = new RT::Queue($RT::SystemUser); + $q->Load($options{ticket_queue}) if $options{ticket_queue}; + my $t = new RT::Ticket($RT::SystemUser); + my $mime = new MIME::Entity; + $mime->build( Type => 'text/plain', Data => $options{ticket_subject} ); + $t->Create( $options{ticket_queue} ? (Queue => $q) : (), + Subject => $options{ticket_subject}, + MIMEObj => $mime, + ); + $t->AddLink( Type => 'MemberOf', + Target => 'freeside://freeside/cust_main/'. $self->custnum, + ); + } + if ($conf->config('welcome_letter') && $self->cust_main->num_pkgs == 1) { my $queue = new FS::queue { 'job' => 'FS::cust_main::queueable_print', diff --git a/FS/FS/cust_recon.pm b/FS/FS/cust_recon.pm new file mode 100644 index 000000000..0a1ca3ae2 --- /dev/null +++ b/FS/FS/cust_recon.pm @@ -0,0 +1,193 @@ +package FS::cust_recon; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs ); + +=head1 NAME + +FS::cust_recon - Object methods for cust_recon records + +=head1 SYNOPSIS + + use FS::cust_recon; + + $record = new FS::cust_recon \%hash; + $record = new FS::cust_recon { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_recon object represents a customer reconcilation. FS::cust_recon +inherits from FS::Record. The following fields are currently supported: + +=over 4 + +=item reconid + +primary key + +=item recondate + +recondate + +=item custnum + +custnum + +=item agentnum + +agentnum + +=item last + +last + +=item first + +first + +=item address1 + +address1 + +=item address2 + +address2 + +=item city + +city + +=item state + +state + +=item zip + +zip + +=item pkg + +pkg + +=item adjourn + +adjourn + +=item status + +status + +=item agent_custid + +agent_custid + +=item agent_pkg + +agent_pkg + +=item agent_adjourn + +agent_adjourn + +=item comments + +comments + + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new customer reconcilation. To add the reconcilation 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 method. + +=cut + +sub table { 'cust_recon'; } + +=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 reconcilation. 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('reconid') + || $self->ut_numbern('recondate') + || $self->ut_number('custnum') + || $self->ut_number('agentnum') + || $self->ut_text('last') + || $self->ut_text('first') + || $self->ut_text('address1') + || $self->ut_textn('address2') + || $self->ut_text('city') + || $self->ut_textn('state') + || $self->ut_textn('zip') + || $self->ut_textn('pkg') + || $self->ut_numbern('adjourn') + || $self->ut_textn('status') + || $self->ut_text('agent_custid') + || $self->ut_textn('agent_pkg') + || $self->ut_numbern('agent_adjourn') + || $self->ut_textn('comments') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +Possibly the existance of this module. + +=head1 SEE ALSO + +L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index d89a68440..1195b1694 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -617,7 +617,7 @@ sub check_chargable { skip_lastapp ); foreach my $opt (grep !exists($flags{option_cache}->{$_}), @opt ) { - $flags{option_cache}->{$opt} = $self->option($opt); + $flags{option_cache}->{$opt} = $self->option($opt, 1); } my %opt = %{ $flags{option_cache} }; diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 8b5c7b9c9..1a42e6517 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -1023,6 +1023,21 @@ sub check { ; return $error if $error; + my $cust_pkg; + local $username_letter = $username_letter; + if ($self->svcnum) { + my $cust_svc = $self->cust_svc + or return "no cust_svc record found for svcnum ". $self->svcnum; + my $cust_pkg = $cust_svc->cust_pkg; + } + if ($self->pkgnum) { + $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );#complain? + } + if ($cust_pkg) { + $username_letter = + $conf->exists('username-letter', $cust_pkg->cust_main->agentnum); + } + my $ulen = $usernamemax || $self->dbdef_table->column('username')->length; if ( $username_uppercase ) { $recref->{username} =~ /^([a-z0-9_\-\.\&\%\:]{$usernamemin,$ulen})$/i diff --git a/FS/MANIFEST b/FS/MANIFEST index b5c9046a6..b77d5d892 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -438,3 +438,5 @@ FS/tax_rate_location.pm t/tax_rate_location.t FS/cust_bill_pkg_tax_rate_location.pm t/cust_bill_pkg_tax_rate_location.t +FS/cust_recon.pm +t/cust_recon.t diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server index 2087e7130..544f307ee 100644 --- a/FS/bin/freeside-selfservice-server +++ b/FS/bin/freeside-selfservice-server @@ -15,9 +15,11 @@ use FS::Daemon qw(daemonize1 drop_root logfile daemonize2 sigint sigterm); use FS::UID qw(adminsuidsetup forksuidsetup); use FS::ClientAPI; use FS::ClientAPI_SessionCache; +use FS::Record qw( qsearch qsearchs ); use FS::Conf; use FS::cust_svc; +use FS::agent; $FREESIDE_LOG = "%%%FREESIDE_LOG%%%"; $FREESIDE_LOCK = "%%%FREESIDE_LOCK%%%"; @@ -97,7 +99,28 @@ while (1) { if ( $keepalives && $keepalive_count++ > 10 ) { $keepalive_count = 0; lock_write; + nstore_fd( { _token => '_keepalive' }, $writer ); + foreach my $agent ( qsearch( 'agent', { disabled => '' } ) ) { + my $config = qsearchs( 'conf', { name => 'selfservice-bulk_ftp_dir', + agentnum => $agent->agentnum, + } ) + or next; + + my $session = + FS::ClientAPI->dispatch( 'Agent/agent_login', + { username => $agent->username, + password => $agent->_password, + } + ); + + nstore_fd( { _token => '_ftp_scan', + dir => $config->value, + session_id => $session->{session_id}, + }, + $writer + ); + } unlock_write; } next; diff --git a/FS/t/cust_recon.t b/FS/t/cust_recon.t new file mode 100644 index 000000000..3724736f4 --- /dev/null +++ b/FS/t/cust_recon.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_recon; +$loaded=1; +print "ok 1\n"; diff --git a/fs_selfservice/FS-SelfService/MANIFEST b/fs_selfservice/FS-SelfService/MANIFEST index a619b2b6c..2e4d3fec4 100644 --- a/fs_selfservice/FS-SelfService/MANIFEST +++ b/fs_selfservice/FS-SelfService/MANIFEST @@ -5,4 +5,5 @@ SelfService.pm SelfService/XMLRPC.pm test.pl freeside-selfservice-clientd +freeside-selfservice-soap-server freeside-selfservice-xmlrpc-server diff --git a/fs_selfservice/FS-SelfService/Makefile.PL b/fs_selfservice/FS-SelfService/Makefile.PL index c078f0865..600c9d5f5 100644 --- a/fs_selfservice/FS-SelfService/Makefile.PL +++ b/fs_selfservice/FS-SelfService/Makefile.PL @@ -5,6 +5,7 @@ WriteMakefile( 'NAME' => 'FS::SelfService', 'VERSION_FROM' => 'SelfService.pm', # finds $VERSION 'EXE_FILES' => [ 'freeside-selfservice-clientd', + 'freeside-selfservice-soap-server', 'freeside-selfservice-xmlrpc-server', ], 'INSTALLSCRIPT' => '/usr/local/sbin', diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index 058955037..322782a3f 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -71,6 +71,8 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'call_time' => 'PrepaidPhone/call_time', 'call_time_nanpa' => 'PrepaidPhone/call_time_nanpa', 'phonenum_balance' => 'PrepaidPhone/phonenum_balance', + 'bulk_processrow' => 'Bulk/processrow', + 'check_username' => 'Bulk/check_username', #sg 'decompify_pkgs' => 'SGNG/decompify_pkgs', 'previous_payment_info' => 'SGNG/previous_payment_info', diff --git a/fs_selfservice/FS-SelfService/freeside-selfservice-clientd b/fs_selfservice/FS-SelfService/freeside-selfservice-clientd index bdc8e1547..0819d9d67 100644 --- a/fs_selfservice/FS-SelfService/freeside-selfservice-clientd +++ b/fs_selfservice/FS-SelfService/freeside-selfservice-clientd @@ -13,6 +13,7 @@ use Storable 2.09 qw(nstore_fd fd_retrieve); use IO::Handle qw(_IONBF); use IO::Select; use IO::File; +use Text::CSV_XS; #STDOUT->setbuf(''); @@ -36,6 +37,7 @@ my $lock_file = "/usr/local/freeside/selfservice$tag.writelock"; $|=1; $SIG{__WARN__} = \&_logmsg; +#$SIG{__DIE__} = sub { &_logmsg(@_); exit }; #read data to be cached or something #warn "$me Reading init data\n" if $Debug; @@ -75,6 +77,8 @@ nstore_fd( { _packet => '_enable_keepalive' } , \*STDOUT ); warn "entering main loop\n" if $Debug; my %kids; +my %ftp_scan_dir; +my %ftp_scan_map; my $s = new IO::Select; $s->add(\*STDIN); @@ -124,7 +128,18 @@ while (1) { : '' ) if $Debug; - if ( exists($kids{$token}) ) { + if ( $token eq '_ftp_scan' ) { + if ( $ftp_scan_dir{$packet->{dir}} ) { + warn "already processing ". $packet->{dir}. "\n" if $Debug; + } else { + $ftp_scan_dir{$packet->{dir}} = 1; + spawn \&ftp_scan, $packet; + } + $undisp = 1; + next; + } + + if ( exists($kids{$token}) ) { warn "sending return packet to $token via $kids{$token}\n" if $Debug; nstore_fd($packet, $kids{$token}); @@ -158,29 +173,11 @@ while (1) { #handle some commands weirdly? $packet->{_token}=$$; - warn "[child-$$] locking write stream\n" if $Debug > 1; - lock_write; - - warn "[child-$$] sending packet to remote server\n" if $Debug > 1; - nstore_fd($packet, \*STDOUT) or die "FATAL: can't send response: $!"; - - warn "[child-$$] flushing write stream\n" if $Debug > 1; - STDOUT->flush or die "FATAL: can't flush: $!"; - - warn "[child-$$] releasing write lock\n" if $Debug > 1; - unlock_write; + my $rv = send_and_wait( $packet ); warn "[child-$$] closing write stream\n" if $Debug > 1; close STDOUT or die "FATAL: can't close write stream: $!"; #??! - warn "[child-$$] waiting for response from parent\n" if $Debug > 1; - my $w = new IO::Select; - $w->add(\*STDIN); - until ( $w->can_read ) { - warn "[child-$$] WARNING: interrupted select: $!\n"; - } - my $rv = fd_retrieve(\*STDIN); - #close STDIN; warn "[child-$$] sending response to local client" if $Debug > 1; @@ -210,13 +207,17 @@ sub reap_kids { if ( $kid > 0 ) { close $kids{$kid}; delete $kids{$kid}; + if ( $ftp_scan_map{$kid} ) { + delete($ftp_scan_dir{$ftp_scan_map{$kid}}); + delete($ftp_scan_map{$kid}); + } } } #warn "done reaping\n"; } sub spawn { - my $coderef = shift; + my ( $coderef, $packet ) = ( shift, shift ); unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') { use Carp; @@ -231,6 +232,7 @@ sub spawn { return; } elsif ($pid) { warn "begat $pid" if $Debug; + $ftp_scan_map{$pid} = $packet->{dir} if $coderef == \&ftp_scan; $kids{$pid} = $kid; #$kids{$pid}->autoflush; return; # I'm the parent @@ -240,7 +242,7 @@ sub spawn { # open(STDIN, "<&Client") || die "can't dup client to stdin"; # open(STDOUT, ">&Client") || die "can't dup client to stdout"; # open(STDERR, ">&STDOUT") || die "can't dup stdout to stderr"; - exit &$coderef(); + exit &$coderef($packet); } sub _logmsg { @@ -254,6 +256,31 @@ sub _logmsg { close $log; } +sub send_and_wait { + my $packet = shift; + + warn "[child-$$] locking write stream\n" if $Debug > 1; + lock_write; + + warn "[child-$$] sending packet to remote server\n" if $Debug > 1; + nstore_fd($packet, \*STDOUT) or die "FATAL: can't send response: $!"; + + warn "[child-$$] flushing write stream\n" if $Debug > 1; + STDOUT->flush or die "FATAL: can't flush: $!"; + + warn "[child-$$] releasing write lock\n" if $Debug > 1; + unlock_write; + + warn "[child-$$] waiting for response from parent\n" if $Debug > 1; + my $w = new IO::Select; + $w->add(\*STDIN); + until ( $w->can_read ) { + warn "[child-$$] WARNING: interrupted select: $!\n"; + } + + fd_retrieve(\*STDIN); +} + sub lock_write { #broken on freebsd? #flock(STDOUT, LOCK_EX) or die "FATAL: can't lock write stream: $!"; @@ -270,3 +297,81 @@ sub unlock_write { flock(LOCKFILE, LOCK_UN) or die "FATAL: can't unlock $lock_file: $!"; } + +sub ftp_scan { + my $packet = shift; + + warn "[child-$$] performing ftp scan" if $Debug > 1; + + warn "[child-$$] packet received:\n". + join('', map { " $_=>$packet->{$_}\n" } keys %$packet ) + if $Debug > 2; + + $packet->{_token}=$$; + + my $dir; + $packet->{dir} =~ /^(.*)$/ && ($dir = $1); # we trust ourselves + opendir(DIR, $dir) or die "failed to open directory $dir: $!\n"; + my @files = grep(/\.csv$/, readdir(DIR)); + closedir(DIR); + + foreach my $file ( @files ) { + warn "Processing $file ...\n"; + my $csv = Text::CSV_XS->new(); + my $err = ""; + my @records = (); + open(CSV, "<$dir/$file") or die "can't open input file for $file: $!\n"; + open(RESULT, ">$dir/result/$file") + or die "can't open result file for $file: $!\n"; + + while () { + if ( $csv->parse($_) ) { + my @columns = $csv->fields(); + push(@records, \@columns); + } else { + $err = $csv->error_input; + last; + } + } + close(CSV); + if ( $err ) { + rename("$dir/$file", "$dir/rejected/$file"); + } else { + foreach my $record ( @records ) { + + $packet->{row} = $record; + $packet->{_packet} = 'Bulk/processrow'; + my $result = send_and_wait( $packet ); + + if ( $result->{error} ) { + my $name; + $record->[1] =~ /^(\w+)$/ && ( $name = $1 ); + + if ($name) { + my $filename = "$dir/rejected/$name"; + open(REC, ">$filename") or die "can't open $filename: $!\n"; + print REC join(',', @$record); + close REC or die $!; + open(ERR, ">$filename.err") or die "can't open $filename.err: $!\n"; + print ERR $result->{error}; + close ERR or die $!; + }else{ + warn "bad agent_custid"; + } + + } + print RESULT $result->{message}, "\n"; + } + + rename("$dir/$file", "$dir/processed/$file"); + warn "$file processed.\n" if $Debug; + } + close(RESULT); + } + + close STDOUT or die "FATAL: can't close write stream: $!"; #??! + + warn "[child-$$] child exiting" if $Debug > 1; + exit; + +} diff --git a/fs_selfservice/FS-SelfService/freeside-selfservice-soap-server b/fs_selfservice/FS-SelfService/freeside-selfservice-soap-server new file mode 100644 index 000000000..869a8aecc --- /dev/null +++ b/fs_selfservice/FS-SelfService/freeside-selfservice-soap-server @@ -0,0 +1,53 @@ +#!/usr/bin/perl -w +# +# freeside-selfservice-soap-server +# + +use strict; +use Fcntl qw(:flock); +use POSIX; +use Getopt::Std; +use SOAP::Transport::HTTP; +use FS::SelfService; + +use vars qw( $opt_p $opt_d $opt_s ); +use vars qw( $DEBUG ); + +getopts("s:p:d"); +$DEBUG = $opt_d; +my $tag = $opt_s ? $opt_s : ''; +$tag = ($opt_s ? ':' : '') . $opt_p ? ':'.$opt_p : ''; + +my $log_file = "/usr/local/freeside/selfservice.soap$tag.log"; + +my $pid = fork; +defined($pid) or die "Can't fork to start: $!"; +print "Started daemon with pid $pid\n" if $pid; +exit if $pid; + +POSIX::setsid(); +open STDIN, "/dev/null" or die "Can't get rid of STDIN"; +open STDOUT, ">/dev/null" or die "Can't get rid of STDOUT"; +open STDERR, ">&STDOUT" or die "Can't get rid of STDERR"; + +$SIG{__WARN__} = \&_logmsg; +$SIG{__DIE__} = sub { &_logmsg(@_); exit }; + +my $daemon = SOAP::Transport::HTTP::Daemon + ->new($opt_s ? (LocalAddr => $opt_s) : (), LocalPort => $opt_p ? $opt_p : 8080) + ->dispatch_to('/usr/local/freeside/SOAP/') #, 'FS::SelfService' + ->objects_by_reference('iZoomOnlineProvisionService') + ->handle; + +warn "Handling request at ", $daemon->url, "\n"; +$daemon->handle; + +sub _logmsg { + chomp( my $msg = shift ); + my $log = new IO::File ">>$log_file"; + flock($log, LOCK_EX); + seek($log, 0, 2); + print $log "[". scalar(localtime). "] [$$] $msg\n"; + flock($log, LOCK_UN); + close $log; +} diff --git a/fs_selfservice/FS-SelfService/iZoomOnlineProvisionService.pm b/fs_selfservice/FS-SelfService/iZoomOnlineProvisionService.pm new file mode 100644 index 000000000..f4c586969 --- /dev/null +++ b/fs_selfservice/FS-SelfService/iZoomOnlineProvisionService.pm @@ -0,0 +1,75 @@ +package iZoomOnlineProvisionService; + +use strict; + +#BEGIN { push @INC, '/usr/lib/perl/5.8.8/' }; +use FS::SelfService qw( bulk_processrow check_username agent_login ); + +=begin WSDL + +_IN agent_username $string agent username +_IN agent_password $string agent password +_IN agent_custid $string customer id in agent system +_IN username $string customer service username +_IN password $string customer service password +_IN daytime $string phone number +_IN first $string first name +_IN last $string last name +_IN address1 $string address line 1 +_IN address2 $string address line 2 +_IN city $string city +_IN state $string state +_IN zip $string zip +_IN pkg $string package name +_IN action $string one of (R|P|D|S)(reconcile, provision, provision with disk, send disk) +_IN adjourn $string day to terminate service +_IN mobile $string mobile phone +_IN sms $string (T|F) acceptable to send SMS messages to mobile? +_IN ship_addr1 $string shipping address line 1 +_IN ship_addr2 $string shipping address line 2 +_IN ship_city $string shipping address city +_IN ship_state $string shipping address state +_IN ship_zip $string shipping address zip +_RETURN @string array [status, message]. status is one of OK, ERR + +=cut + +my $DEBUG = 0; + +sub Provision { + my $class = shift; + + my $session = agent_login( map { $_ => shift @_ } qw( username password ) ); + return [ 'ERR', $session->{error} ] if $session->{error}; + + my $result = + bulk_processrow( session_id => $session->{session_id}, row => [ @_ ] ); + + return $result->{error} ? [ 'ERR', $result->{error} ] + : [ 'OK', $result->{message} ]; +} + +=begin WSDL + +_IN agent_username $string agent username +_IN agent_password $string agent password +_IN username $string customer service username +_IN domain $string user domain name +_RETURN @string [OK|ERR] + +=cut +sub CheckUserName { + my $class = shift; + + my $session = agent_login( map { $_ => shift @_ } qw( username password ) ); + return [ 'ERR', $session->{error} ] if $session->{error}; + + my $result = check_username( session_id => $session->{session_id}, + map { $_ => shift @_ } qw( user domain ) + ); + + return $result->{error} ? [ 'ERR', $result->{error} ] + : [ 'OK', $result->{message} ]; +} + +1; -- 2.11.0