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 list_customer_packages OPTION => VALUE, ...
655 Lists all customer packages.
671 my $result = FS::API->list_packages(
672 'secret' => 'sharingiscaring',
673 'custnum' => custnum,
676 if ( $result->{'error'} ) {
677 die $result->{'error'};
679 # list packages returns an array of hashes for packages ordered by custnum and pkgnum.
680 print Dumper($result->{'pkgs'});
685 sub list_customer_packages {
686 my( $class, %opt ) = @_;
687 return _shared_secret_error() unless _check_shared_secret($opt{secret});
689 my $sql_query = FS::cust_pkg->search({ 'custnum' => $opt{custnum}, });
691 $sql_query->{order_by} = 'ORDER BY custnum, pkgnum';
693 my @packages = qsearch($sql_query)
694 or return { 'error' => 'No packages' };
697 'packages' => [ map $_->hashref, @packages ],
703 =item package_status OPTION => VALUE, ...
721 my $result = FS::API->package_status(
722 'secret' => 'sharingiscaring',
726 if ( $result->{'error'} ) {
727 die $result->{'error'};
729 # package status returns a hash with the status for a package.
730 print Dumper($result->{'status'});
736 my( $class, %opt ) = @_;
737 return _shared_secret_error() unless _check_shared_secret($opt{secret});
739 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{pkgnum} } )
740 or return { 'error' => 'No packages' };
743 'status' => $cust_pkg->status,
749 =item order_package OPTION => VALUE, ...
751 Orders a new customer package. Takes a list of keys and values as paramaters
752 with the following keys:
786 Including this implements per-customer custom pricing for this package, overriding package definition pricing
790 Including this implements per-customer custom pricing for this package, overriding package definition pricing
792 =item invoice_details
794 A single string for just one detail line, or an array reference of one or more
800 my( $class, %opt ) = @_;
802 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
803 or return { 'error' => 'Unknown custnum' };
805 #some conceptual false laziness w/cust_pkg/Import.pm
807 my $cust_pkg = new FS::cust_pkg {
808 'pkgpart' => $opt{'pkgpart'},
809 'quantity' => $opt{'quantity'} || 1,
812 #start_date and contract_end
813 foreach my $date_field (qw( start_date contract_end )) {
814 if ( $opt{$date_field} =~ /^(\d+)$/ ) {
815 $cust_pkg->$date_field( $opt{$date_field} );
816 } elsif ( $opt{$date_field} ) {
817 $cust_pkg->$date_field( str2time( $opt{$date_field} ) );
821 #especially this part for custom pkg price
822 # (false laziness w/cust_pkg/Import.pm)
823 my $s = $opt{'setup_fee'};
824 my $r = $opt{'recur_fee'};
825 my $part_pkg = $cust_pkg->part_pkg;
826 if ( ( length($s) && $s != $part_pkg->option('setup_fee') )
827 or ( length($r) && $r != $part_pkg->option('recur_fee') )
831 local($FS::part_pkg::skip_pkg_svc_hack) = 1;
833 my $custom_part_pkg = $part_pkg->clone;
834 $custom_part_pkg->disabled('Y');
835 my %options = $part_pkg->options;
836 $options{'setup_fee'} = $s if length($s);
837 $options{'recur_fee'} = $r if length($r);
838 my $error = $custom_part_pkg->insert( options=>\%options );
839 return ( 'error' => "error customizing package: $error" ) if $error;
841 #not ->pkg_svc, we want to ignore links and clone the actual package def
842 foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
843 my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
844 $c_pkg_svc->pkgsvcnum('');
845 $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
846 my $error = $c_pkg_svc->insert;
847 return "error customizing package: $error" if $error;
850 $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
854 my %order_pkg = ( 'cust_pkg' => $cust_pkg );
856 my @loc_fields = qw( address1 address2 city county state zip country );
857 if ( grep length($opt{$_}), @loc_fields ) {
858 $order_pkg{'cust_location'} = new FS::cust_location {
859 map { $_ => $opt{$_} } @loc_fields, 'custnum'
863 $order_pkg{'invoice_details'} = $opt{'invoice_details'}
864 if $opt{'invoice_details'};
866 my $error = $cust_main->order_pkg( %order_pkg );
869 return { 'error' => $error,
873 # return { 'error' => '',
874 # #cust_main->order_pkg doesn't actually have a way to return pkgnum
875 # #'pkgnum' => $pkgnum,
881 =item change_package_location
883 Updates package location. Takes a list of keys and values
884 as parameters with the following keys:
890 locationnum - pass this, or the following keys (don't pass both)
922 On error, returns a hashref with an 'error' key.
923 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
924 containing the new values.
928 sub change_package_location {
931 return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
933 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
934 or return { 'error' => 'Unknown pkgnum' };
938 foreach my $field ( qw(
956 $changeopt{$field} = $opt{$field} if $opt{$field};
959 $cust_pkg->API_change(%changeopt);
962 =item bill_now OPTION => VALUE, ...
964 Bills a single customer now, in the same fashion as the "Bill now" link in the
967 Returns a hash reference with a single key, 'error'. If there is an error,
968 the value contains the error, otherwise it is empty. Takes a list of keys and
969 values as parameters with the following keys:
975 API Secret (required)
979 Customer number (required)
986 my( $class, %opt ) = @_;
987 return _shared_secret_error() unless _check_shared_secret($opt{secret});
989 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
990 or return { 'error' => 'Unknown custnum' };
992 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
997 return { 'error' => $error,
1003 #next.. Delete Advertising sources?
1005 =item list_advertising_sources OPTION => VALUE, ...
1007 Lists all advertising sources.
1019 my $result = FS::API->list_advertising_sources(
1020 'secret' => 'sharingiscaring',
1023 if ( $result->{'error'} ) {
1024 die $result->{'error'};
1026 # list advertising sources returns an array of hashes for sources.
1027 print Dumper($result->{'sources'});
1032 #list_advertising_sources
1033 sub list_advertising_sources {
1034 my( $class, %opt ) = @_;
1035 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1037 my @sources = qsearch('part_referral', {}, '', "")
1038 or return { 'error' => 'No referrals' };
1041 'sources' => [ map $_->hashref, @sources ],
1047 =item add_advertising_source OPTION => VALUE, ...
1049 Add a new advertising source.
1063 Referral disabled, Y for disabled or nothing for enabled
1071 External referral ID
1077 my $result = FS::API->add_advertising_source(
1078 'secret' => 'sharingiscaring',
1079 'referral' => 'test referral',
1083 'agentnum' => '2', #agent id number
1084 'title' => 'test title',
1087 if ( $result->{'error'} ) {
1088 die $result->{'error'};
1090 # add_advertising_source returns new source upon success.
1091 print Dumper($result);
1096 #add_advertising_source
1097 sub add_advertising_source {
1098 my( $class, %opt ) = @_;
1099 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1101 use FS::part_referral;
1103 my $new_source = $opt{source};
1105 my $source = new FS::part_referral $new_source;
1107 my $error = $source->insert;
1109 my $return = {$source->hash};
1110 $return = { 'error' => $error, } if $error;
1115 =item edit_advertising_source OPTION => VALUE, ...
1117 Edit a advertising source.
1127 Referral number to edit
1131 hash of edited source fields.
1141 Referral disabled, Y for disabled or nothing for enabled
1149 External referral ID
1157 my $result = FS::API->edit_advertising_source(
1158 'secret' => 'sharingiscaring',
1159 'refnum' => '4', # referral number to edit
1162 'referral' => 'test referral',
1164 'agentnum' => '2', #agent id number
1165 'title' => 'test title',
1169 if ( $result->{'error'} ) {
1170 die $result->{'error'};
1172 # edit_advertising_source returns updated source upon success.
1173 print Dumper($result);
1178 #edit_advertising_source
1179 sub edit_advertising_source {
1180 my( $class, %opt ) = @_;
1181 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1183 use FS::part_referral;
1185 my $refnum = $opt{refnum};
1186 my $source = $opt{source};
1188 my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1189 my $new = new FS::part_referral { $old->hash };
1191 foreach my $key (keys %$source) {
1192 $new->$key($source->{$key});
1195 my $error = $new->replace;
1197 my $return = {$new->hash};
1198 $return = { 'error' => $error, } if $error;
1205 # helper subroutines
1208 sub _check_shared_secret {
1209 shift eq FS::Conf->new->config('api_shared_secret');
1212 sub _shared_secret_error {
1213 return { 'error' => 'Incorrect shared secret' };