6 use FS::Record qw( qsearch qsearchs );
16 FS::API - Freeside backend API
23 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
26 my $xmlrpc = new Frontier::Client url=>$url;
28 my $result = $xmlrpc->call( 'FS.API.customer_info',
29 'secret' => 'sharingiscaring',
33 print Dumper($result);
37 This module implements a backend API for advanced back-office integration.
39 In contrast to the self-service API, which authenticates an end-user and offers
40 functionality to that end user, the backend API performs a simple shared-secret
41 authentication and offers full, administrator functionality, enabling
42 integration with other back-office systems. Only access this API from a secure
43 network from other backoffice machines. DON'T use this API to create customer
46 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
47 the port by default, only allow access from back-office servers with the same
48 security precations as the Freeside server, and encrypt the communication
49 channel (for example, with an SSH tunnel or VPN) rather than accessing it
56 =item insert_payment OPTION => VALUE, ...
58 Adds a new payment to a customers account. Takes a list of keys and values as
59 paramters with the following keys:
81 Option date for payment
91 my $result = FS::API->insert_payment(
92 'secret' => 'sharingiscaring',
98 '_date' => 1397977200, #UNIX timestamp
99 'order_number' => '12345',
102 if ( $result->{'error'} ) {
103 die $result->{'error'};
105 #payment was inserted
106 print "paynum ". $result->{'paynum'};
113 my($class, %opt) = @_;
114 return _shared_secret_error() unless _check_shared_secret($opt{secret});
116 #less "raw" than this? we are the backoffice API, and aren't worried
117 # about version migration ala cust_main/cust_location here
118 my $cust_pay = new FS::cust_pay { %opt };
119 my $error = $cust_pay->insert( 'manual'=>1 );
120 return { 'error' => $error,
121 'paynum' => $cust_pay->paynum,
125 # pass the phone number ( from svc_phone )
126 sub insert_payment_phonenum {
127 my($class, %opt) = @_;
128 $class->_by_phonenum('insert_payment', %opt);
132 my($class, $method, %opt) = @_;
133 return _shared_secret_error() unless _check_shared_secret($opt{secret});
135 my $phonenum = delete $opt{'phonenum'};
137 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
138 or return { 'error' => 'Unknown phonenum' };
140 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
141 or return { 'error' => 'Unlinked phonenum' };
143 $opt{'custnum'} = $cust_pkg->custnum;
145 $class->$method(%opt);
148 =item insert_credit OPTION => VALUE, ...
150 Adds a a credit to a customers account. Takes a list of keys and values as
151 parameters with the following keys
169 The date the credit will be posted
175 my $result = FS::API->insert_credit(
176 'secret' => 'sharingiscaring',
181 '_date' => 1397977200, #UNIX timestamp
184 if ( $result->{'error'} ) {
185 die $result->{'error'};
188 print "crednum ". $result->{'crednum'};
195 my($class, %opt) = @_;
196 return _shared_secret_error() unless _check_shared_secret($opt{secret});
198 $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
200 #less "raw" than this? we are the backoffice API, and aren't worried
201 # about version migration ala cust_main/cust_location here
202 my $cust_credit = new FS::cust_credit { %opt };
203 my $error = $cust_credit->insert;
204 return { 'error' => $error,
205 'crednum' => $cust_credit->crednum,
209 # pass the phone number ( from svc_phone )
210 sub insert_credit_phonenum {
211 my($class, %opt) = @_;
212 $class->_by_phonenum('insert_credit', %opt);
215 =item apply_payments_and_credits
217 Applies payments and credits for this customer. Takes a list of keys and
218 values as parameter with the following keys:
234 #apply payments and credits
235 sub apply_payments_and_credits {
236 my($class, %opt) = @_;
237 return _shared_secret_error() unless _check_shared_secret($opt{secret});
239 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
240 or return { 'error' => 'Unknown custnum' };
242 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
243 return { 'error' => $error, };
246 =item insert_refund OPTION => VALUE, ...
248 Adds a a credit to a customers account. Takes a list of keys and values as
249 parmeters with the following keys: custnum, payby, refund
253 my $result = FS::API->insert_refund(
254 'secret' => 'sharingiscaring',
260 '_date' => 1397977200, #UNIX timestamp
263 if ( $result->{'error'} ) {
264 die $result->{'error'};
267 print "refundnum ". $result->{'crednum'};
274 my($class, %opt) = @_;
275 return _shared_secret_error() unless _check_shared_secret($opt{secret});
277 # when github pull request #24 is merged,
278 # will have to change over to default reasonnum like credit
279 # but until then, this will do
280 $opt{'reason'} ||= 'API refund';
282 #less "raw" than this? we are the backoffice API, and aren't worried
283 # about version migration ala cust_main/cust_location here
284 my $cust_refund = new FS::cust_refund { %opt };
285 my $error = $cust_refund->insert;
286 return { 'error' => $error,
287 'refundnum' => $cust_refund->refundnum,
291 # pass the phone number ( from svc_phone )
292 sub insert_refund_phonenum {
293 my($class, %opt) = @_;
294 $class->_by_phonenum('insert_refund', %opt);
299 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
300 # figure out how to trigger something when those things change
302 # long-term: package changes?
304 =item new_customer OPTION => VALUE, ...
306 Creates a new customer. Takes a list of keys and values as parameters with the
317 first name (required)
325 (not typically collected; mostly used for ACH transactions)
331 =item address1 (required)
335 =item city (required)
343 =item state (required)
365 Currently used for third party tax vendor lookups
369 Used for determining FCC 477 reporting
373 Used for determining FCC 477 reporting
393 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),
395 Set to 1 to enable postal invoicing
397 =item referral_custnum
399 Referring customer number
411 Agent specific customer number
413 =item referral_custnum
415 Referring customer number
421 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
422 # but approaching this from a clean start / back-office perspective
423 # i.e. no package/service, no immediate credit card run, etc.
426 my( $class, %opt ) = @_;
427 return _shared_secret_error() unless _check_shared_secret($opt{secret});
429 #default agentnum like signup_server-default_agentnum?
430 #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
432 #same for refnum like signup_server-default_refnum
433 $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
435 $class->API_insert( %opt );
438 =item update_customer
440 Updates an existing customer. Passing an empty value clears that field, while
441 NOT passing that key/value at all leaves it alone. Takes a list of keys and
442 values as parameters with the following keys:
448 API Secret (required)
452 Customer number (required)
508 Comma-separated list of email addresses for email invoices. The special value
509 'POST' is used to designate postal invoicing (it may be specified alone or in
510 addition to email addresses),
512 Set to 1 to enable postal invoicing
514 =item referral_custnum
516 Referring customer number
530 sub update_customer {
531 my( $class, %opt ) = @_;
532 return _shared_secret_error() unless _check_shared_secret($opt{secret});
534 FS::cust_main->API_update( %opt );
537 =item customer_info OPTION => VALUE, ...
539 Returns general customer information. Takes a list of keys and values as
540 parameters with the following keys: custnum, secret
544 use Frontier::Client;
547 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
550 my $xmlrpc = new Frontier::Client url=>$url;
552 my $result = $xmlrpc->call( 'FS.API.customer_info',
553 'secret' => 'sharingiscaring',
557 print Dumper($result);
562 my( $class, %opt ) = @_;
563 return _shared_secret_error() unless _check_shared_secret($opt{secret});
565 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
566 or return { 'error' => 'Unknown custnum' };
568 $cust_main->API_getinfo;
571 =item customer_list_svcs OPTION => VALUE, ...
573 Returns customer service information. Takes a list of keys and values as
574 parameters with the following keys: custnum, secret
578 use Frontier::Client;
581 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
584 my $xmlrpc = new Frontier::Client url=>$url;
586 my $result = $xmlrpc->call( 'FS.API.customer_list_svcs',
587 'secret' => 'sharingiscaring',
591 print Dumper($result);
593 foreach my $cust_svc ( @{ $result->{'cust_svc'} } ) {
594 #print $cust_svc->{mac_addr}."\n" if exists $cust_svc->{mac_addr};
595 print $cust_svc->{circuit_id}."\n" if exists $cust_svc->{circuit_id};
600 sub customer_list_svcs {
601 my( $class, %opt ) = @_;
602 return _shared_secret_error() unless _check_shared_secret($opt{secret});
604 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
605 or return { 'error' => 'Unknown custnum' };
607 #$cust_main->API_list_svcs;
609 #false laziness w/ClientAPI/list_svcs
612 #my @cust_pkg_usage = ();
613 #foreach my $cust_pkg ( $p->{'ncancelled'}
614 # ? $cust_main->ncancelled_pkgs
615 # : $cust_main->unsuspended_pkgs ) {
616 foreach my $cust_pkg ( $cust_main->all_pkgs ) {
617 #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
618 push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
619 #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
623 'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
630 Returns location specific information for the customer. Takes a list of keys
631 and values as paramters with the following keys: custnum, secret
635 #I also monitor for changes to the additional locations that are applied to
636 # packages, and would like for those to be exportable as well. basically the
637 # location data passed with the custnum.
640 my( $class, %opt ) = @_;
641 return _shared_secret_error() unless _check_shared_secret($opt{secret});
643 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
647 'locations' => [ map $_->hashref, @cust_location ],
653 =item order_package OPTION => VALUE, ...
655 Orders a new customer package. Takes a list of keys and values as paramaters
656 with the following keys:
690 Including this implements per-customer custom pricing for this package, overriding package definition pricing
694 Including this implements per-customer custom pricing for this package, overriding package definition pricing
696 =item invoice_details
698 A single string for just one detail line, or an array reference of one or more
704 my( $class, %opt ) = @_;
706 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
707 or return { 'error' => 'Unknown custnum' };
709 #some conceptual false laziness w/cust_pkg/Import.pm
711 my $cust_pkg = new FS::cust_pkg {
712 'pkgpart' => $opt{'pkgpart'},
713 'quantity' => $opt{'quantity'} || 1,
716 #start_date and contract_end
717 foreach my $date_field (qw( start_date contract_end )) {
718 if ( $opt{$date_field} =~ /^(\d+)$/ ) {
719 $cust_pkg->$date_field( $opt{$date_field} );
720 } elsif ( $opt{$date_field} ) {
721 $cust_pkg->$date_field( str2time( $opt{$date_field} ) );
725 #especially this part for custom pkg price
726 # (false laziness w/cust_pkg/Import.pm)
727 my $s = $opt{'setup_fee'};
728 my $r = $opt{'recur_fee'};
729 my $part_pkg = $cust_pkg->part_pkg;
730 if ( ( length($s) && $s != $part_pkg->option('setup_fee') )
731 or ( length($r) && $r != $part_pkg->option('recur_fee') )
734 my $custom_part_pkg = $part_pkg->clone;
735 $custom_part_pkg->disabled('Y');
736 my %options = $part_pkg->options;
737 $options{'setup_fee'} = $s if length($s);
738 $options{'recur_fee'} = $r if length($r);
739 my $error = $custom_part_pkg->insert( options=>\%options );
740 return ( 'error' => "error customizing package: $error" ) if $error;
741 $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
744 my %order_pkg = ( 'cust_pkg' => $cust_pkg );
746 my @loc_fields = qw( address1 address2 city county state zip country );
747 if ( grep length($opt{$_}), @loc_fields ) {
748 $order_pkg{'cust_location'} = new FS::cust_location {
749 map { $_ => $opt{$_} } @loc_fields, 'custnum'
753 $order_pkg{'invoice_details'} = $opt{'invoice_details'}
754 if $opt{'invoice_details'};
756 my $error = $cust_main->order_pkg( %order_pkg );
759 return { 'error' => $error,
763 # return { 'error' => '',
764 # #cust_main->order_pkg doesn't actually have a way to return pkgnum
765 # #'pkgnum' => $pkgnum,
771 =item change_package_location
773 Updates package location. Takes a list of keys and values
774 as parameters with the following keys:
780 locationnum - pass this, or the following keys (don't pass both)
812 On error, returns a hashref with an 'error' key.
813 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
814 containing the new values.
818 sub change_package_location {
821 return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
823 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
824 or return { 'error' => 'Unknown pkgnum' };
828 foreach my $field ( qw(
846 $changeopt{$field} = $opt{$field} if $opt{$field};
849 $cust_pkg->API_change(%changeopt);
852 =item bill_now OPTION => VALUE, ...
854 Bills a single customer now, in the same fashion as the "Bill now" link in the
857 Returns a hash reference with a single key, 'error'. If there is an error,
858 the value contains the error, otherwise it is empty. Takes a list of keys and
859 values as parameters with the following keys:
865 API Secret (required)
869 Customer number (required)
876 my( $class, %opt ) = @_;
877 return _shared_secret_error() unless _check_shared_secret($opt{secret});
879 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
880 or return { 'error' => 'Unknown custnum' };
882 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
887 return { 'error' => $error,
893 #next.. Delete Advertising sources?
895 =item list_advertising_sources OPTION => VALUE, ...
897 Lists all advertising sources.
909 my $result = FS::API->list_advertising_sources(
910 'secret' => 'sharingiscaring',
913 if ( $result->{'error'} ) {
914 die $result->{'error'};
916 # list advertising sources returns an array of hashes for sources.
917 print Dumper($result->{'sources'});
922 #list_advertising_sources
923 sub list_advertising_sources {
924 my( $class, %opt ) = @_;
925 return _shared_secret_error() unless _check_shared_secret($opt{secret});
927 my @sources = qsearch('part_referral', {}, '', "")
928 or return { 'error' => 'No referrals' };
931 'sources' => [ map $_->hashref, @sources ],
937 =item add_advertising_source OPTION => VALUE, ...
939 Add a new advertising source.
953 Referral disabled, Y for disabled or nothing for enabled
967 my $result = FS::API->add_advertising_source(
968 'secret' => 'sharingiscaring',
969 'referral' => 'test referral',
973 'agentnum' => '2', #agent id number
974 'title' => 'test title',
977 if ( $result->{'error'} ) {
978 die $result->{'error'};
980 # add_advertising_source returns new source upon success.
981 print Dumper($result);
986 #add_advertising_source
987 sub add_advertising_source {
988 my( $class, %opt ) = @_;
989 return _shared_secret_error() unless _check_shared_secret($opt{secret});
991 use FS::part_referral;
993 my $new_source = $opt{source};
995 my $source = new FS::part_referral $new_source;
997 my $error = $source->insert;
999 my $return = {$source->hash};
1000 $return = { 'error' => $error, } if $error;
1005 =item edit_advertising_source OPTION => VALUE, ...
1007 Edit a advertising source.
1017 Referral number to edit
1021 hash of edited source fields.
1031 Referral disabled, Y for disabled or nothing for enabled
1039 External referral ID
1047 my $result = FS::API->edit_advertising_source(
1048 'secret' => 'sharingiscaring',
1049 'refnum' => '4', # referral number to edit
1052 'referral' => 'test referral',
1054 'agentnum' => '2', #agent id number
1055 'title' => 'test title',
1059 if ( $result->{'error'} ) {
1060 die $result->{'error'};
1062 # edit_advertising_source returns updated source upon success.
1063 print Dumper($result);
1068 #edit_advertising_source
1069 sub edit_advertising_source {
1070 my( $class, %opt ) = @_;
1071 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1073 use FS::part_referral;
1075 my $refnum = $opt{refnum};
1076 my $source = $opt{source};
1078 my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1079 my $new = new FS::part_referral { $old->hash };
1081 foreach my $key (keys %$source) {
1082 $new->$key($source->{$key});
1085 my $error = $new->replace;
1087 my $return = {$new->hash};
1088 $return = { 'error' => $error, } if $error;
1095 # helper subroutines
1098 sub _check_shared_secret {
1099 shift eq FS::Conf->new->config('api_shared_secret');
1102 sub _shared_secret_error {
1103 return { 'error' => 'Incorrect shared secret' };