5 use FS::Record qw( qsearch qsearchs );
15 FS::API - Freeside backend API
22 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
25 my $xmlrpc = new Frontier::Client url=>$url;
27 my $result = $xmlrpc->call( 'FS.API.customer_info',
28 'secret' => 'sharingiscaring',
32 print Dumper($result);
36 This module implements a backend API for advanced back-office integration.
38 In contrast to the self-service API, which authenticates an end-user and offers
39 functionality to that end user, the backend API performs a simple shared-secret
40 authentication and offers full, administrator functionality, enabling
41 integration with other back-office systems. Only access this API from a secure
42 network from other backoffice machines. DON'T use this API to create customer
45 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
46 the port by default, only allow access from back-office servers with the same
47 security precations as the Freeside server, and encrypt the communication
48 channel (for example, with an SSH tunnel or VPN) rather than accessing it
55 =item insert_payment OPTION => VALUE, ...
57 Adds a new payment to a customers account. Takes a list of keys and values as
58 paramters with the following keys:
80 Option date for payment
90 my $result = FS::API->insert_payment(
91 'secret' => 'sharingiscaring',
97 '_date' => 1397977200, #UNIX timestamp
98 'order_number' => '12345',
101 if ( $result->{'error'} ) {
102 die $result->{'error'};
104 #payment was inserted
105 print "paynum ". $result->{'paynum'};
112 my($class, %opt) = @_;
113 return _shared_secret_error() unless _check_shared_secret($opt{secret});
115 #less "raw" than this? we are the backoffice API, and aren't worried
116 # about version migration ala cust_main/cust_location here
117 my $cust_pay = new FS::cust_pay { %opt };
118 my $error = $cust_pay->insert( 'manual'=>1 );
119 return { 'error' => $error,
120 'paynum' => $cust_pay->paynum,
124 # pass the phone number ( from svc_phone )
125 sub insert_payment_phonenum {
126 my($class, %opt) = @_;
127 $class->_by_phonenum('insert_payment', %opt);
131 my($class, $method, %opt) = @_;
132 return _shared_secret_error() unless _check_shared_secret($opt{secret});
134 my $phonenum = delete $opt{'phonenum'};
136 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
137 or return { 'error' => 'Unknown phonenum' };
139 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
140 or return { 'error' => 'Unlinked phonenum' };
142 $opt{'custnum'} = $cust_pkg->custnum;
144 $class->$method(%opt);
147 =item insert_credit OPTION => VALUE, ...
149 Adds a a credit to a customers account. Takes a list of keys and values as
150 parameters with the following keys
168 The date the credit will be posted
174 my $result = FS::API->insert_credit(
175 'secret' => 'sharingiscaring',
180 '_date' => 1397977200, #UNIX timestamp
183 if ( $result->{'error'} ) {
184 die $result->{'error'};
187 print "crednum ". $result->{'crednum'};
194 my($class, %opt) = @_;
195 return _shared_secret_error() unless _check_shared_secret($opt{secret});
197 $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
199 #less "raw" than this? we are the backoffice API, and aren't worried
200 # about version migration ala cust_main/cust_location here
201 my $cust_credit = new FS::cust_credit { %opt };
202 my $error = $cust_credit->insert;
203 return { 'error' => $error,
204 'crednum' => $cust_credit->crednum,
208 # pass the phone number ( from svc_phone )
209 sub insert_credit_phonenum {
210 my($class, %opt) = @_;
211 $class->_by_phonenum('insert_credit', %opt);
214 =item apply_payments_and_credits
216 Applies payments and credits for this customer. Takes a list of keys and
217 values as parameter with the following keys:
233 #apply payments and credits
234 sub apply_payments_and_credits {
235 my($class, %opt) = @_;
236 return _shared_secret_error() unless _check_shared_secret($opt{secret});
238 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
239 or return { 'error' => 'Unknown custnum' };
241 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
242 return { 'error' => $error, };
245 =item insert_refund OPTION => VALUE, ...
247 Adds a a credit to a customers account. Takes a list of keys and values as
248 parmeters with the following keys: custnum, payby, refund
252 my $result = FS::API->insert_refund(
253 'secret' => 'sharingiscaring',
259 '_date' => 1397977200, #UNIX timestamp
262 if ( $result->{'error'} ) {
263 die $result->{'error'};
266 print "refundnum ". $result->{'crednum'};
273 my($class, %opt) = @_;
274 return _shared_secret_error() unless _check_shared_secret($opt{secret});
276 # when github pull request #24 is merged,
277 # will have to change over to default reasonnum like credit
278 # but until then, this will do
279 $opt{'reason'} ||= 'API refund';
281 #less "raw" than this? we are the backoffice API, and aren't worried
282 # about version migration ala cust_main/cust_location here
283 my $cust_refund = new FS::cust_refund { %opt };
284 my $error = $cust_refund->insert;
285 return { 'error' => $error,
286 'refundnum' => $cust_refund->refundnum,
290 # pass the phone number ( from svc_phone )
291 sub insert_refund_phonenum {
292 my($class, %opt) = @_;
293 $class->_by_phonenum('insert_refund', %opt);
298 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
299 # figure out how to trigger something when those things change
301 # long-term: package changes?
303 =item new_customer OPTION => VALUE, ...
305 Creates a new customer. Takes a list of keys and values as parameters with the
316 first name (required)
324 (not typically collected; mostly used for ACH transactions)
330 =item address1 (required)
334 =item city (required)
342 =item state (required)
364 Currently used for third party tax vendor lookups
368 Used for determining FCC 477 reporting
372 Used for determining FCC 477 reporting
392 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),
394 Set to 1 to enable postal invoicing
396 =item referral_custnum
398 Referring customer number
410 Agent specific customer number
412 =item referral_custnum
414 Referring customer number
420 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
421 # but approaching this from a clean start / back-office perspective
422 # i.e. no package/service, no immediate credit card run, etc.
425 my( $class, %opt ) = @_;
426 return _shared_secret_error() unless _check_shared_secret($opt{secret});
428 #default agentnum like signup_server-default_agentnum?
429 #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
431 #same for refnum like signup_server-default_refnum
432 $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
434 $class->API_insert( %opt );
437 =item update_customer
439 Updates an existing customer. Passing an empty value clears that field, while
440 NOT passing that key/value at all leaves it alone. Takes a list of keys and
441 values as parameters with the following keys:
447 API Secret (required)
451 Customer number (required)
507 Comma-separated list of email addresses for email invoices. The special value
508 'POST' is used to designate postal invoicing (it may be specified alone or in
509 addition to email addresses),
511 Set to 1 to enable postal invoicing
513 =item referral_custnum
515 Referring customer number
529 sub update_customer {
530 my( $class, %opt ) = @_;
531 return _shared_secret_error() unless _check_shared_secret($opt{secret});
533 FS::cust_main->API_update( %opt );
536 =item customer_info OPTION => VALUE, ...
538 Returns general customer information. Takes a list of keys and values as
539 parameters with the following keys: custnum, secret
543 use Frontier::Client;
546 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
549 my $xmlrpc = new Frontier::Client url=>$url;
551 my $result = $xmlrpc->call( 'FS.API.customer_info',
552 'secret' => 'sharingiscaring',
556 print Dumper($result);
561 my( $class, %opt ) = @_;
562 return _shared_secret_error() unless _check_shared_secret($opt{secret});
564 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
565 or return { 'error' => 'Unknown custnum' };
567 $cust_main->API_getinfo;
570 =item customer_list_svcs OPTION => VALUE, ...
572 Returns customer service information. Takes a list of keys and values as
573 parameters with the following keys: custnum, secret
577 use Frontier::Client;
580 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
583 my $xmlrpc = new Frontier::Client url=>$url;
585 my $result = $xmlrpc->call( 'FS.API.customer_list_svcs',
586 'secret' => 'sharingiscaring',
590 print Dumper($result);
592 foreach my $cust_svc ( @{ $result->{'cust_svc'} } ) {
593 #print $cust_svc->{mac_addr}."\n" if exists $cust_svc->{mac_addr};
594 print $cust_svc->{circuit_id}."\n" if exists $cust_svc->{circuit_id};
599 sub customer_list_svcs {
600 my( $class, %opt ) = @_;
601 return _shared_secret_error() unless _check_shared_secret($opt{secret});
603 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
604 or return { 'error' => 'Unknown custnum' };
606 #$cust_main->API_list_svcs;
608 #false laziness w/ClientAPI/list_svcs
611 #my @cust_pkg_usage = ();
612 #foreach my $cust_pkg ( $p->{'ncancelled'}
613 # ? $cust_main->ncancelled_pkgs
614 # : $cust_main->unsuspended_pkgs ) {
615 foreach my $cust_pkg ( $cust_main->all_pkgs ) {
616 #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
617 push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
618 #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
622 'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
629 Returns location specific information for the customer. Takes a list of keys
630 and values as paramters with the following keys: custnum, secret
634 #I also monitor for changes to the additional locations that are applied to
635 # packages, and would like for those to be exportable as well. basically the
636 # location data passed with the custnum.
639 my( $class, %opt ) = @_;
640 return _shared_secret_error() unless _check_shared_secret($opt{secret});
642 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
646 'locations' => [ map $_->hashref, @cust_location ],
652 =item change_package_location
654 Updates package location. Takes a list of keys and values
655 as paramters with the following keys:
661 locationnum - pass this, or the following keys (don't pass both)
693 On error, returns a hashref with an 'error' key.
694 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
695 containing the new values.
699 sub change_package_location {
702 return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
704 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
705 or return { 'error' => 'Unknown pkgnum' };
709 foreach my $field ( qw(
727 $changeopt{$field} = $opt{$field} if $opt{$field};
730 $cust_pkg->API_change(%changeopt);
733 =item bill_now OPTION => VALUE, ...
735 Bills a single customer now, in the same fashion as the "Bill now" link in the
738 Returns a hash reference with a single key, 'error'. If there is an error,
739 the value contains the error, otherwise it is empty. Takes a list of keys and
740 values as parameters with the following keys:
746 API Secret (required)
750 Customer number (required)
757 my( $class, %opt ) = @_;
758 return _shared_secret_error() unless _check_shared_secret($opt{secret});
760 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
761 or return { 'error' => 'Unknown custnum' };
763 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
768 return { 'error' => $error,
774 #next.. Delete Advertising sources?
776 =item list_advertising_sources OPTION => VALUE, ...
778 Lists all advertising sources.
790 my $result = FS::API->list_advertising_sources(
791 'secret' => 'sharingiscaring',
794 if ( $result->{'error'} ) {
795 die $result->{'error'};
797 # list advertising sources returns an array of hashes for sources.
798 print Dumper($result->{'sources'});
803 #list_advertising_sources
804 sub list_advertising_sources {
805 my( $class, %opt ) = @_;
806 return _shared_secret_error() unless _check_shared_secret($opt{secret});
808 my @sources = qsearch('part_referral', {}, '', "")
809 or return { 'error' => 'No referrals' };
812 'sources' => [ map $_->hashref, @sources ],
818 =item add_advertising_source OPTION => VALUE, ...
820 Add a new advertising source.
834 Referral disabled, Y for disabled or nothing for enabled
848 my $result = FS::API->add_advertising_source(
849 'secret' => 'sharingiscaring',
850 'referral' => 'test referral',
854 'agentnum' => '2', #agent id number
855 'title' => 'test title',
858 if ( $result->{'error'} ) {
859 die $result->{'error'};
861 # add_advertising_source returns new source upon success.
862 print Dumper($result);
867 #add_advertising_source
868 sub add_advertising_source {
869 my( $class, %opt ) = @_;
870 return _shared_secret_error() unless _check_shared_secret($opt{secret});
872 use FS::part_referral;
874 my $new_source = $opt{source};
876 my $source = new FS::part_referral $new_source;
878 my $error = $source->insert;
880 my $return = {$source->hash};
881 $return = { 'error' => $error, } if $error;
886 =item edit_advertising_source OPTION => VALUE, ...
888 Edit a advertising source.
898 Referral number to edit
902 hash of edited source fields.
912 Referral disabled, Y for disabled or nothing for enabled
928 my $result = FS::API->edit_advertising_source(
929 'secret' => 'sharingiscaring',
930 'refnum' => '4', # referral number to edit
933 'referral' => 'test referral',
935 'agentnum' => '2', #agent id number
936 'title' => 'test title',
940 if ( $result->{'error'} ) {
941 die $result->{'error'};
943 # edit_advertising_source returns updated source upon success.
944 print Dumper($result);
949 #edit_advertising_source
950 sub edit_advertising_source {
951 my( $class, %opt ) = @_;
952 return _shared_secret_error() unless _check_shared_secret($opt{secret});
954 use FS::part_referral;
956 my $refnum = $opt{refnum};
957 my $source = $opt{source};
959 my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
960 my $new = new FS::part_referral { $old->hash };
962 foreach my $key (keys %$source) {
963 $new->$key($source->{$key});
966 my $error = $new->replace;
968 my $return = {$new->hash};
969 $return = { 'error' => $error, } if $error;
979 sub _check_shared_secret {
980 shift eq FS::Conf->new->config('api_shared_secret');
983 sub _shared_secret_error {
984 return { 'error' => 'Incorrect shared secret' };