package FS::API; use strict; use FS::Conf; use FS::Record qw( qsearch qsearchs ); use FS::cust_main; use FS::cust_location; use FS::cust_pay; use FS::cust_credit; use FS::cust_refund; use FS::cust_pkg; =head1 NAME FS::API - Freeside backend API =head1 SYNOPSIS use FS::API; =head1 DESCRIPTION This module implements a backend API for advanced back-office integration. In contrast to the self-service API, which authenticates an end-user and offers functionality to that end user, the backend API performs a simple shared-secret authentication and offers full, administrator functionality, enabling integration with other back-office systems. Only access this API from a secure network from other backoffice machines. DON'T use this API to create customer portal functionality. If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block the port by default, only allow access from back-office servers with the same security precations as the Freeside server, and encrypt the communication channel (for example, with an SSH tunnel or VPN) rather than accessing it in plaintext. =head1 METHODS =over 4 =item insert_payment OPTION => VALUE, ... Adds a new payment to a customers account. Takes a list of keys and values as paramters with the following keys: =over 4 =item secret API Secret =item custnum Customer number =item payby Payment type =item paid Amount paid =item _date Option date for payment =item order_number Optional order number =back Example: my $result = FS::API->insert_payment( 'secret' => 'sharingiscaring', 'custnum' => 181318, 'payby' => 'CASH', 'paid' => '54.32', #optional '_date' => 1397977200, #UNIX timestamp 'order_number' => '12345', ); if ( $result->{'error'} ) { die $result->{'error'}; } else { #payment was inserted print "paynum ". $result->{'paynum'}; } =cut #enter cash payment sub insert_payment { my($class, %opt) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); #less "raw" than this? we are the backoffice API, and aren't worried # about version migration ala cust_main/cust_location here my $cust_pay = new FS::cust_pay { %opt }; my $error = $cust_pay->insert( 'manual'=>1 ); return { 'error' => $error, 'paynum' => $cust_pay->paynum, }; } # pass the phone number ( from svc_phone ) sub insert_payment_phonenum { my($class, %opt) = @_; $class->_by_phonenum('insert_payment', %opt); } sub _by_phonenum { my($class, $method, %opt) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); my $phonenum = delete $opt{'phonenum'}; my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } ) or return { 'error' => 'Unknown phonenum' }; my $cust_pkg = $svc_phone->cust_svc->cust_pkg or return { 'error' => 'Unlinked phonenum' }; $opt{'custnum'} = $cust_pkg->custnum; $class->$method(%opt); } =item insert_credit OPTION => VALUE, ... Adds a a credit to a customers account. Takes a list of keys and values as parameters with the following keys =over =item secret API Secret =item custnum customer number =item amount Amount of the credit =item _date The date the credit will be posted =back Example: my $result = FS::API->insert_credit( 'secret' => 'sharingiscaring', 'custnum' => 181318, 'amount' => '54.32', #optional '_date' => 1397977200, #UNIX timestamp ); if ( $result->{'error'} ) { die $result->{'error'}; } else { #credit was inserted print "crednum ". $result->{'crednum'}; } =cut #Enter credit sub insert_credit { my($class, %opt) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason'); #less "raw" than this? we are the backoffice API, and aren't worried # about version migration ala cust_main/cust_location here my $cust_credit = new FS::cust_credit { %opt }; my $error = $cust_credit->insert; return { 'error' => $error, 'crednum' => $cust_credit->crednum, }; } # pass the phone number ( from svc_phone ) sub insert_credit_phonenum { my($class, %opt) = @_; $class->_by_phonenum('insert_credit', %opt); } =item apply_payments_and_credits Applies payments and credits for this customer. Takes a list of keys and values as parameter with the following keys: =over 4 =item secret API secret =item custnum Customer number =back =cut #apply payments and credits sub apply_payments_and_credits { my($class, %opt) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} }) or return { 'error' => 'Unknown custnum' }; my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 ); return { 'error' => $error, }; } =item insert_refund OPTION => VALUE, ... Adds a a credit to a customers account. Takes a list of keys and values as parmeters with the following keys: custnum, payby, refund Example: my $result = FS::API->insert_refund( 'secret' => 'sharingiscaring', 'custnum' => 181318, 'payby' => 'CASH', 'refund' => '54.32', #optional '_date' => 1397977200, #UNIX timestamp ); if ( $result->{'error'} ) { die $result->{'error'}; } else { #refund was inserted print "refundnum ". $result->{'crednum'}; } =cut #Enter cash refund. sub insert_refund { my($class, %opt) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); # when github pull request #24 is merged, # will have to change over to default reasonnum like credit # but until then, this will do $opt{'reason'} ||= 'API refund'; #less "raw" than this? we are the backoffice API, and aren't worried # about version migration ala cust_main/cust_location here my $cust_refund = new FS::cust_refund { %opt }; my $error = $cust_refund->insert; return { 'error' => $error, 'refundnum' => $cust_refund->refundnum, }; } # pass the phone number ( from svc_phone ) sub insert_refund_phonenum { my($class, %opt) = @_; $class->_by_phonenum('insert_refund', %opt); } #--- # "2 way syncing" ? start with non-sync pulling info here, then if necessary # figure out how to trigger something when those things change # long-term: package changes? =item new_customer OPTION => VALUE, ... Creates a new customer. Takes a list of keys and values as parameters with the following keys: =over 4 =item secret API Secret =item first first name (required) =item last last name (required) =item ss (not typically collected; mostly used for ACH transactions) =item company Company name =item address1 (required) Address line one =item city (required) City =item county County =item state (required) State =item zip (required) Zip or postal code =item country 2 Digit Country Code =item latitude latitude =item Longitude longitude =item geocode Currently used for third party tax vendor lookups =item censustract Used for determining FCC 477 reporting =item censusyear Used for determining FCC 477 reporting =item daytime Daytime phone number =item night Evening phone number =item fax Fax number =item mobile Mobile number =item invoicing_list comma-separated list of email addresses for email invoices. The special value 'POST' is used to designate postal invoicing (it may be specified alone or in addition to email addresses), postal_invoicing Set to 1 to enable postal invoicing =item referral_custnum Referring customer number =item salesnum Sales person number =item agentnum Agent number =item agent_custid Agent specific customer number =item referral_custnum Referring customer number =back =cut #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal # but approaching this from a clean start / back-office perspective # i.e. no package/service, no immediate credit card run, etc. sub new_customer { my( $class, %opt ) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); #default agentnum like signup_server-default_agentnum? #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum'); #same for refnum like signup_server-default_refnum $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum'); $class->API_insert( %opt ); } =item update_customer Updates an existing customer. Passing an empty value clears that field, while NOT passing that key/value at all leaves it alone. Takes a list of keys and values as parameters with the following keys: =over 4 =item secret API Secret (required) =item custnum Customer number (required) =item first first name =item last last name =item company Company name =item address1 Address line one =item city City =item county County =item state State =item zip Zip or postal code =item country 2 Digit Country Code =item daytime Daytime phone number =item night Evening phone number =item fax Fax number =item mobile Mobile number =item invoicing_list Comma-separated list of email addresses for email invoices. The special value 'POST' is used to designate postal invoicing (it may be specified alone or in addition to email addresses), postal_invoicing Set to 1 to enable postal invoicing =item referral_custnum Referring customer number =item salesnum Sales person number =item agentnum Agent number =back =cut sub update_customer { my( $class, %opt ) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); FS::cust_main->API_update( %opt ); } =item customer_info OPTION => VALUE, ... Returns general customer information. Takes a list of keys and values as parameters with the following keys: custnum, secret =cut sub customer_info { my( $class, %opt ) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} }) or return { 'error' => 'Unknown custnum' }; $cust_main->API_getinfo; } =item location_info Returns location specific information for the customer. Takes a list of keys and values as paramters with the following keys: custnum, secret =cut #I also monitor for changes to the additional locations that are applied to # packages, and would like for those to be exportable as well. basically the # location data passed with the custnum. sub location_info { my( $class, %opt ) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} }); my %return = ( 'error' => '', 'locations' => [ map $_->hashref, @cust_location ], ); return \%return; } =item change_package_location Updates package location. Takes a list of keys and values as paramters with the following keys: pkgnum secret locationnum - pass this, or the following keys (don't pass both) locationname address1 address2 city county state zip addr_clean country censustract censusyear location_type location_number location_kind incorporated On error, returns a hashref with an 'error' key. On success, returns a hashref with 'pkgnum' and 'locationnum' keys, containing the new values. =cut sub change_package_location { my $class = shift; my %opt = @_; return _shared_secret_error() unless _check_shared_secret($opt{'secret'}); my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} }) or return { 'error' => 'Unknown pkgnum' }; my %changeopt; foreach my $field ( qw( locationnum locationname address1 address2 city county state zip addr_clean country censustract censusyear location_type location_number location_kind incorporated )) { $changeopt{$field} = $opt{$field} if $opt{$field}; } $cust_pkg->API_change(%changeopt); } =item bill_now OPTION => VALUE, ... Bills a single customer now, in the same fashion as the "Bill now" link in the UI. Returns a hash reference with a single key, 'error'. If there is an error, the value contains the error, otherwise it is empty. Takes a list of keys and values as parameters with the following keys: =over 4 =item secret API Secret (required) =item custnum Customer number (required) =back =cut sub bill_now { my( $class, %opt ) = @_; return _shared_secret_error() unless _check_shared_secret($opt{secret}); my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} }) or return { 'error' => 'Unknown custnum' }; my $error = $cust_main->bill_and_collect( 'fatal' => 'return', 'retry' => 1, 'check_freq' =>'1d', ); return { 'error' => $error, }; } #next.. Advertising sources? ## # helper subroutines ## sub _check_shared_secret { shift eq FS::Conf->new->config('api_shared_secret'); } sub _shared_secret_error { return { 'error' => 'Incorrect shared secret' }; } 1;