X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2FClientAPI%2FSignup.pm;h=94c9c5e840c4c527d82e6156892f903e139e7636;hb=eb8ed18a57d255741003d7d861786eada7970f50;hp=593c426b59e3ff351939ea1fc738fe588f38eaef;hpb=4ed0a352b2319c2fc14ee7af09a21caf727e7b12;p=freeside.git diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index 593c426b5..94c9c5e84 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -2,10 +2,12 @@ package FS::ClientAPI::Signup; use strict; use vars qw( $DEBUG $me ); +use subs qw( _myaccount_cache ); use Data::Dumper; use Tie::RefHash; +use Digest::SHA qw(sha512_hex); use FS::Conf; -use FS::Record qw(qsearch qsearchs dbdef); +use FS::Record qw(qsearch qsearchs dbdef dbh); use FS::CGI qw(popurl); use FS::Msgcat qw(gettext); use FS::Misc qw(card_types); @@ -23,10 +25,31 @@ use FS::queue; use FS::reg_code; use FS::payby; use FS::banned_pay; +use FS::part_tag; +use FS::cust_payby; $DEBUG = 1; $me = '[FS::ClientAPI::Signup]'; +=head1 NAME + +FS::ClientAPI::Signup - Front-end API for signing up customers + +=head1 DESCRIPTION + +This module provides the ClientAPI functions for talking to a signup +server. The signup server is open to the public, i.e. does not require a +login. The back-end Freeside server creates customers, orders packages and +services, and processes initial payments. + +=head1 METHODS + +=over 4 + +=cut + +# document the rest of this as we work on it + sub clear_cache { warn "$me clear_cache called\n" if $DEBUG; my $cache = new FS::ClientAPI_SessionCache( { @@ -172,10 +195,12 @@ sub signup_info { 'nomadix' => $conf->exists('signup_server-nomadix'), 'payby' => [ $conf->config('signup_server-payby') ], 'card_types' => card_types(), - 'paytypes' => [ @FS::cust_main::paytypes ], + 'paytypes' => [ FS::cust_payby->paytypes ], 'cvv_enabled' => 1, + 'require_cvv' => $conf->exists('signup-require_cvv'), 'stateid_enabled' => $conf->exists('show_stateid'), 'paystate_enabled' => $conf->exists('show_bankstate'), + 'exempt_groups' => [ grep /\S/, $conf->config('tax-cust_exempt-groups') ], 'ship_enabled' => 1, 'msgcat' => $msgcat, 'label' => $label, @@ -493,21 +518,8 @@ sub new_customer { #possibly some validation will be needed } - my $agentnum; - if ( exists $packet->{'session_id'} ) { - my $cache = new FS::ClientAPI_SessionCache( { - 'namespace' => 'FS::ClientAPI::Agent', - } ); - my $session = $cache->get($packet->{'session_id'}); - if ( $session ) { - $agentnum = $session->{'agentnum'}; - } else { - return { 'error' => "Can't resume session" }; #better error message - } - } else { - $agentnum = $packet->{agentnum} - || $conf->config('signup_server-default_agentnum'); - } + my $agentnum = get_agentnum($packet); + return $agentnum if ref($agentnum); my ($bill_hash, $ship_hash); foreach my $f (FS::cust_main->location_fields) { @@ -518,6 +530,37 @@ sub new_customer { #shares some stuff with htdocs/edit/process/cust_main.cgi... take any # common that are still here and library them. + + my %cust_main = ( + 'agentnum' => $agentnum, + 'refnum' => $packet->{refnum} + || $conf->config('signup_server-default_refnum'), + 'tagnum' => [ FS::part_tag->default_tags ], + + ( map { $_ => $packet->{$_} } qw( + salesnum + ss stateid stateid_state + locale + referral_custnum comments + ) + ), + + ); + + my %insert_options = (); + if ( $packet->{payby} =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) { + $insert_options{cust_payby} = [ + new FS::cust_payby { + map { $_ => $packet->{$_} } qw( + payby + payinfo paycvv paydate payname paystate paytype + paystart_month paystart_year payissue + payip + ), + } + ]; + } + my $template_custnum = $conf->config('signup_server-prepaid-template-custnum'); my $cust_main; if ( $template_custnum && $packet->{prepaid_shortform} ) { @@ -525,27 +568,10 @@ sub new_customer { my $template_cust = qsearchs('cust_main', { 'custnum' => $template_custnum } ); return { 'error' => 'Configuration error' } unless $template_cust; $cust_main = new FS::cust_main ( { - 'agentnum' => $agentnum, - 'refnum' => $packet->{refnum} - || $conf->config('signup_server-default_refnum'), - - ( map { $_ => $template_cust->$_ } qw( - last first company daytime night fax - ) - ), - - ( map { $_ => $packet->{$_} } qw( - ss stateid stateid_state - - payby - payinfo paycvv paydate payname paystate paytype - paystart_month paystart_year payissue - payip - - referral_custnum comments - ) + %cust_main, + map { $_ => $template_cust->$_ } qw( + last first company daytime night fax mobile ), - } ); $bill_hash = { $template_cust->bill_location->location_hash }; @@ -554,22 +580,11 @@ sub new_customer { } else { $cust_main = new FS::cust_main ( { - #'custnum' => '', - 'agentnum' => $agentnum, - 'refnum' => $packet->{refnum} - || $conf->config('signup_server-default_refnum'), - + %cust_main, map { $_ => $packet->{$_} } qw( - last first ss company - daytime night fax stateid stateid_state - payby - payinfo paycvv paydate payname paystate paytype - paystart_month paystart_year payissue - payip + last first company daytime night fax mobile override_ban_warn - referral_custnum comments ), - } ); } @@ -627,17 +642,25 @@ sub new_customer { ); } - $cust_main->payby('BILL') # MCRD better? + $cust_main->payby('BILL') # MCRD better? no, that's for something else if $gw && $gw->gateway_namespace eq 'Business::OnlineThirdPartyPayment'; } - $cust_main->payinfo($cust_main->daytime) - if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo; + return { 'error' => "CVV2 is required" } + if $cust_main->payby =~ /^(CARD|DCRD)$/ + && ! $cust_main->paycvv + && $conf->exists('signup-require_cvv'); my @invoicing_list = $packet->{'invoicing_list'} ? split( /\s*\,\s*/, $packet->{'invoicing_list'} ) : (); + my @exempt_groups = grep /\S/, $conf->config('tax-cust_exempt-groups'); + my @tax_exempt = grep { $packet->{"tax_$_"} eq 'Y' } @exempt_groups; + $insert_options{'tax_exemption'} = { + map { $_ => $packet->{"tax_$_".'_num'} } @tax_exempt + }; + $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/; my $pkgpart = $1; return { 'error' => 'Please select a package' } unless $pkgpart; #msgcat @@ -645,7 +668,6 @@ sub new_customer { my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } ) or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" }; - my $svcpart = $part_pkg->svcpart($svc_x); my $reg_code = ''; if ( $packet->{'reg_code'} ) { @@ -663,50 +685,58 @@ sub new_customer { #my $error = $cust_pkg->check; #return { 'error' => $error } if $error; - #should be all auto-magic and shit my @svc = (); - if ( $svc_x eq 'svc_acct' ) { + unless ( $svc_x eq 'none' ) { - my $svc = new FS::svc_acct { - 'svcpart' => $svcpart, - map { $_ => $packet->{$_} } - qw( username _password sec_phrase popnum domsvc ), - }; + my $svcpart = $part_pkg->svcpart($svc_x); + #should be all auto-magic and shit + if ( $svc_x eq 'svc_acct' ) { - my @acct_snarf; - my $snarfnum = 1; - while ( exists($packet->{"snarf_machine$snarfnum"}) - && length($packet->{"snarf_machine$snarfnum"}) ) { - my $acct_snarf = new FS::acct_snarf ( { - 'machine' => $packet->{"snarf_machine$snarfnum"}, - 'protocol' => $packet->{"snarf_protocol$snarfnum"}, - 'username' => $packet->{"snarf_username$snarfnum"}, - '_password' => $packet->{"snarf_password$snarfnum"}, - } ); - $snarfnum++; - push @acct_snarf, $acct_snarf; - } - $svc->child_objects( \@acct_snarf ); - push @svc, $svc; + my $svc = new FS::svc_acct { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( username _password sec_phrase popnum domsvc ), + }; + + my $error = $svc->is_password_allowed($packet->{_password}); + return { error => $error } if $error; + + my @acct_snarf; + my $snarfnum = 1; + while ( exists($packet->{"snarf_machine$snarfnum"}) + && length($packet->{"snarf_machine$snarfnum"}) ) { + my $acct_snarf = new FS::acct_snarf ( { + 'machine' => $packet->{"snarf_machine$snarfnum"}, + 'protocol' => $packet->{"snarf_protocol$snarfnum"}, + 'username' => $packet->{"snarf_username$snarfnum"}, + '_password' => $packet->{"snarf_password$snarfnum"}, + } ); + $snarfnum++; + push @acct_snarf, $acct_snarf; + } + $svc->child_objects( \@acct_snarf ); + push @svc, $svc; - } elsif ( $svc_x eq 'svc_phone' ) { + } elsif ( $svc_x eq 'svc_phone' ) { - push @svc, new FS::svc_phone ( { - 'svcpart' => $svcpart, - map { $_ => $packet->{$_} } - qw( countrycode phonenum sip_password pin ), - } ); + push @svc, new FS::svc_phone ( { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( countrycode phonenum sip_password pin ), + } ); - } elsif ( $svc_x eq 'svc_pbx' ) { + } elsif ( $svc_x eq 'svc_pbx' ) { - push @svc, new FS::svc_pbx ( { - 'svcpart' => $svcpart, - map { $_ => $packet->{$_} } - qw( id title ), - } ); + push @svc, new FS::svc_pbx ( { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( id title ), + } ); - } else { - die "unknown signup service $svc_x"; + } else { + die "unknown signup service $svc_x"; + } + } if ($packet->{'mac_addr'} && $conf->exists('signup_server-mac_addr_svcparts')) @@ -753,6 +783,7 @@ sub new_customer { \%hash, \@invoicing_list, 'depend_jobnum' => $placeholder->jobnum, + %insert_options, ); if ( $error ) { my $perror = $placeholder->delete; @@ -764,7 +795,11 @@ sub new_customer { #warn "$me Billing customer...\n" if $Debug; - my $bill_error = $cust_main->bill( 'depend_jobnum'=>$placeholder->jobnum ); + my @cust_bill; + my $bill_error = $cust_main->bill( + 'depend_jobnum' => $placeholder->jobnum, + 'return_bill' => \@cust_bill, + ); #warn "$me error billing new customer: $bill_error" # if $bill_error; @@ -799,11 +834,11 @@ sub new_customer { if ( $cust_main->balance > 0 ) { - #this makes sense. credit is "un-doing" the invoice - $cust_main->credit( $cust_main->balance, 'signup server decline', - 'reason_type' => $conf->config('signup_credit_type'), - ); - $cust_main->apply_credits; + #this used to apply a credit, but now we can void invoices... + foreach my $cust_bill (@cust_bill) { + my $voiderror = $cust_bill->void('automatic payment failed'); + warn "Error voiding cust bill after decline: $voiderror" if $voiderror; + } #should check list for errors... #$cust_main->suspend; @@ -872,6 +907,7 @@ sub new_customer { #false laziness w/ above # fresh restart to support "free account" portals with 3.x/4.x-style # addressless accounts +# and a contact (for self-service login) sub new_customer_minimal { my $packet = shift; @@ -894,111 +930,157 @@ sub new_customer_minimal { #possibly some validation will be needed } - my $agentnum; - if ( exists $packet->{'session_id'} ) { - my $cache = new FS::ClientAPI_SessionCache( { - 'namespace' => 'FS::ClientAPI::Agent', - } ); - my $session = $cache->get($packet->{'session_id'}); - if ( $session ) { - $agentnum = $session->{'agentnum'}; - } else { - return { 'error' => "Can't resume session" }; #better error message - } - } else { - $agentnum = $packet->{agentnum} - || $conf->config('signup_server-default_agentnum'); - } + my $agentnum = get_agentnum($packet); + return $agentnum if ref($agentnum); #shares some stuff with htdocs/edit/process/cust_main.cgi... take any # common that are still here and library them. my $cust_main = new FS::cust_main ( { - #'custnum' => '', - 'agentnum' => $agentnum, - 'refnum' => $packet->{refnum} - || $conf->config('signup_server-default_refnum'), - 'payby' => 'BILL', + 'agentnum' => $agentnum, + 'refnum' => $packet->{refnum} + || $conf->config('signup_server-default_refnum'), + 'tagnum' => [ FS::part_tag->default_tags ], map { $_ => $packet->{$_} } qw( - last first ss company - daytime night fax + salesnum + last first company daytime night fax mobile + ss stateid stateid_state + + locale ), } ); + my %opt = (); + if ( $packet->{payby} =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) { + $opt{cust_payby} = [ + new FS::cust_payby { + map { $_ => $packet->{$_} } qw( + payby + payinfo paycvv paydate payname paystate paytype + paystart_month paystart_year payissue + payip + ), + } + ]; + } + + if ( grep length($packet->{$_}), FS::cust_main->location_fields ) { + my $bill_hash; + foreach my $f (FS::cust_main->location_fields) { + $bill_hash->{$f} = $packet->{$f}; + } + my $bill_location = FS::cust_location->new($bill_hash); + $cust_main->set('bill_location' => $bill_location); + $cust_main->set('ship_location' => $bill_location); + } + my @invoicing_list = $packet->{'invoicing_list'} ? split( /\s*\,\s*/, $packet->{'invoicing_list'} ) : (); + use Tie::RefHash; + tie my %hash, 'Tie::RefHash', (); + my @svc = (); + $packet->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/; my $pkgpart = $1; - return { 'error' => 'Please select a package' } unless $pkgpart; #msgcat - my $part_pkg = - qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } ) - or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" }; - my $svcpart = $part_pkg->svcpart($svc_x); + if ( $pkgpart ) { - my $cust_pkg = new FS::cust_pkg ( { - #later#'custnum' => $custnum, - 'pkgpart' => $packet->{'pkgpart'}, - } ); - #my $error = $cust_pkg->check; - #return { 'error' => $error } if $error; + my $part_pkg = + qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } ) + or return { 'error' => "WARNING: unknown pkgpart: $pkgpart" }; - #should be all auto-magic and shit - my @svc = (); - if ( $svc_x eq 'svc_acct' ) { + my $cust_pkg = new FS::cust_pkg ( { + #later#'custnum' => $custnum, + 'pkgpart' => $packet->{'pkgpart'}, + } ); + #my $error = $cust_pkg->check; + #return { 'error' => $error } if $error; - my $svc = new FS::svc_acct { - 'svcpart' => $svcpart, - map { $_ => $packet->{$_} } - qw( username _password sec_phrase popnum domsvc ), - }; + unless ( $svc_x eq 'none' ) { - push @svc, $svc; + my $svcpart = $part_pkg->svcpart($svc_x); + #should be all auto-magic and shit + if ( $svc_x eq 'svc_acct' ) { - } elsif ( $svc_x eq 'svc_phone' ) { + my $svc = new FS::svc_acct { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( username _password sec_phrase popnum domsvc ), + }; - push @svc, new FS::svc_phone ( { - 'svcpart' => $svcpart, - map { $_ => $packet->{$_} } - qw( countrycode phonenum sip_password pin ), - } ); + push @svc, $svc; - } elsif ( $svc_x eq 'svc_pbx' ) { + } elsif ( $svc_x eq 'svc_phone' ) { - push @svc, new FS::svc_pbx ( { - 'svcpart' => $svcpart, - map { $_ => $packet->{$_} } - qw( id title ), + push @svc, new FS::svc_phone ( { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( countrycode phonenum sip_password pin ), } ); - - } else { - die "unknown signup service $svc_x"; + + } elsif ( $svc_x eq 'svc_pbx' ) { + + push @svc, new FS::svc_pbx ( { + 'svcpart' => $svcpart, + map { $_ => $packet->{$_} } + qw( id title ), + } ); + + } else { + die "unknown signup service $svc_x"; + } + + } + + foreach my $svc ( @svc ) { + my $y = $svc->setdefault; # arguably should be in new method + return { 'error' => $y } if $y && !ref($y); + #$error = $svc->check; + #return { 'error' => $error } if $error; + } + + use Tie::RefHash; + tie my %hash, 'Tie::RefHash'; + $hash{ $cust_pkg } = \@svc; + } - foreach my $svc ( @svc ) { - my $y = $svc->setdefault; # arguably should be in new method - return { 'error' => $y } if $y && !ref($y); - #$error = $svc->check; - #return { 'error' => $error } if $error; + if ( $invoicing_list[0] && $packet->{'_password'} ) { + $opt{'contact'} = [ + new FS::contact { 'first' => $cust_main->first, + 'last' => $cust_main->get('last'), + '_password' => $packet->{'_password'}, + 'emailaddress' => $invoicing_list[0], + 'selfservice_access' => 'Y', + } + ]; } - use Tie::RefHash; - tie my %hash, 'Tie::RefHash'; - %hash = ( $cust_pkg => \@svc ); - #msgcat my $error = $cust_main->insert( \%hash, \@invoicing_list, + %opt, ); return { 'error' => $error } if $error; + my $session = { 'custnum' => $cust_main->custnum }; + + my $session_id; + do { + $session_id = sha512_hex(time(). {}. rand(). $$) + } until ( ! defined _myaccount_cache->get($session_id) ); #just in case + + _myaccount_cache->set( $session_id, $session, '1 hour' ); # 1 hour? + my %return = ( 'error' => '', 'signup_service' => $svc_x, 'custnum' => $cust_main->custnum, + 'session_id' => $session_id, + map { $_ => $cust_main->$_ } qw( first last company ), ); if ( $svc[0] ) { @@ -1023,6 +1105,13 @@ sub new_customer_minimal { } +use vars qw( $myaccount_cache ); +sub _myaccount_cache { + $myaccount_cache ||= new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::MyAccount', + } ); +} + sub capture_payment { my $packet = shift; @@ -1124,4 +1213,128 @@ sub capture_payment { } +=item get_agentnum PACKET + +Given a PACKET from the signup server, looks up the agentnum to use for signing +up a customer. This will use 'session_id' if the agent is authenticated, +otherwise 'agentnum', otherwise the 'signup_server-default_agentnum' config. If +the agent can't be found, returns an error packet. + +=cut + +sub get_agentnum { + my $packet = shift; + my $conf = new FS::Conf; + my $agentnum; + if ( exists $packet->{'session_id'} ) { + my $cache = new FS::ClientAPI_SessionCache( { + 'namespace' => 'FS::ClientAPI::Agent', + } ); + my $session = $cache->get($packet->{'session_id'}); + if ( $session ) { + $agentnum = $session->{'agentnum'}; + } else { + return { 'error' => "Can't resume session" }; #better error message + } + } else { + $agentnum = $packet->{agentnum} + || $conf->config('signup_server-default_agentnum'); + } + if ( $agentnum and FS::agent->count('agentnum = ?', $agentnum) ) { + return $agentnum; + } + return { 'error' => 'Signup is not configured' }; +} + +=item new_prospect PACKET + +Creates a new L entry. PACKET must contain: + +- either agentnum or session_id (unless signup_server-default_agentnum exists) + +- refnum (unless signup_server-default_refnum exists) + +- last and first (names), and optionally company and title + +- address1, city, state, country, zip, and optionally address2 + +- emailaddress + +- one or more of phone_daytime, phone_night, phone_mobile, and phone_fax + +=cut + +sub new_prospect { + + my $packet = shift; + warn "$me new_prospect called\n".Dumper($packet) if $DEBUG; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + my $conf = FS::Conf->new; + + my $agentnum = get_agentnum($packet); + return $agentnum if ref $agentnum; + my $refnum = $packet->{refnum} + || $conf->config('signup_server-default_refnum'); + + my $prospect = FS::prospect_main->new({ + 'agentnum' => $agentnum, + 'refnum' => $refnum, + 'company' => $packet->{company}, + }); + + my $location = FS::cust_location->new; + foreach ( qw(address1 address2 city county state zip country ) ) { + $location->set($_, $packet->{$_}); + } + $prospect->set('cust_location', $location); + + my $error = $prospect->insert; # also does location + return { error => $error } if $error; + + my $contact = FS::contact->new({ + prospectnum => $prospect->prospectnum, + locationnum => $location->locationnum, + invoice_dest => 'Y', + }); + # use emailaddress pseudo-field behavior here + foreach (qw(last first title emailaddress)) { + $contact->set($_, $packet->{$_}); + } + $error = $contact->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return { error => $error }; + } + + foreach my $phone_type (qsearch('phone_type', {})) { + my $key = 'phone_' . lc($phone_type->typename); + my $phonenum = $packet->{$key}; + if ( $phonenum ) { + # just to not have to supply country code from the other end + my $number = Number::Phone->new($location->country, $phonenum); + if (!$number) { + $error = 'invalid phone number'; + } else { + my $phone = FS::contact_phone->new({ + contactnum => $contact->contactnum, + phonenum => $phonenum, + countrycode => $number->country_code, + phonetypenum => $phone_type->phonetypenum, + }); + $error = $phone->insert; + } + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return { error => $phone_type->typename . ' phone: ' . $error }; + } + } + } # foreach $phone_type + + $dbh->commit if $oldAutoCommit; + return { prospectnum => $prospect->prospectnum }; +} + 1;