6 use FS::Record qw( qsearch qsearchs );
17 FS::API - Freeside backend API
24 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
27 my $xmlrpc = new Frontier::Client url=>$url;
29 my $result = $xmlrpc->call( 'FS.API.customer_info',
30 'secret' => 'sharingiscaring',
34 print Dumper($result);
38 This module implements a backend API for advanced back-office integration.
40 In contrast to the self-service API, which authenticates an end-user and offers
41 functionality to that end user, the backend API performs a simple shared-secret
42 authentication and offers full, administrator functionality, enabling
43 integration with other back-office systems. Only access this API from a secure
44 network from other backoffice machines. DON'T use this API to create customer
47 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
48 the port by default, only allow access from back-office servers with the same
49 security precations as the Freeside server, and encrypt the communication
50 channel (for example, with an SSH tunnel or VPN) rather than accessing it
57 =item insert_payment OPTION => VALUE, ...
59 Adds a new payment to a customers account. Takes a list of keys and values as
60 paramters with the following keys:
82 Option date for payment
92 my $result = FS::API->insert_payment(
93 'secret' => 'sharingiscaring',
99 '_date' => 1397977200, #UNIX timestamp
100 'order_number' => '12345',
103 if ( $result->{'error'} ) {
104 die $result->{'error'};
106 #payment was inserted
107 print "paynum ". $result->{'paynum'};
114 my($class, %opt) = @_;
115 return _shared_secret_error() unless _check_shared_secret($opt{secret});
117 #less "raw" than this? we are the backoffice API, and aren't worried
118 # about version migration ala cust_main/cust_location here
119 my $cust_pay = new FS::cust_pay { %opt };
120 my $error = $cust_pay->insert( 'manual'=>1 );
121 return { 'error' => $error,
122 'paynum' => $cust_pay->paynum,
126 # pass the phone number ( from svc_phone )
127 sub insert_payment_phonenum {
128 my($class, %opt) = @_;
129 $class->_by_phonenum('insert_payment', %opt);
133 my($class, $method, %opt) = @_;
134 return _shared_secret_error() unless _check_shared_secret($opt{secret});
136 my $phonenum = delete $opt{'phonenum'};
138 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
139 or return { 'error' => 'Unknown phonenum' };
141 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
142 or return { 'error' => 'Unlinked phonenum' };
144 $opt{'custnum'} = $cust_pkg->custnum;
146 $class->$method(%opt);
149 =item insert_credit OPTION => VALUE, ...
151 Adds a a credit to a customers account. Takes a list of keys and values as
152 parameters with the following keys
170 The date the credit will be posted
176 my $result = FS::API->insert_credit(
177 'secret' => 'sharingiscaring',
182 '_date' => 1397977200, #UNIX timestamp
185 if ( $result->{'error'} ) {
186 die $result->{'error'};
189 print "crednum ". $result->{'crednum'};
196 my($class, %opt) = @_;
197 return _shared_secret_error() unless _check_shared_secret($opt{secret});
199 $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
201 #less "raw" than this? we are the backoffice API, and aren't worried
202 # about version migration ala cust_main/cust_location here
203 my $cust_credit = new FS::cust_credit { %opt };
204 my $error = $cust_credit->insert;
205 return { 'error' => $error,
206 'crednum' => $cust_credit->crednum,
210 # pass the phone number ( from svc_phone )
211 sub insert_credit_phonenum {
212 my($class, %opt) = @_;
213 $class->_by_phonenum('insert_credit', %opt);
216 =item apply_payments_and_credits
218 Applies payments and credits for this customer. Takes a list of keys and
219 values as parameter with the following keys:
235 #apply payments and credits
236 sub apply_payments_and_credits {
237 my($class, %opt) = @_;
238 return _shared_secret_error() unless _check_shared_secret($opt{secret});
240 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
241 or return { 'error' => 'Unknown custnum' };
243 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
244 return { 'error' => $error, };
247 =item insert_refund OPTION => VALUE, ...
249 Adds a a credit to a customers account. Takes a list of keys and values as
250 parmeters with the following keys: custnum, payby, refund
254 my $result = FS::API->insert_refund(
255 'secret' => 'sharingiscaring',
261 '_date' => 1397977200, #UNIX timestamp
264 if ( $result->{'error'} ) {
265 die $result->{'error'};
268 print "refundnum ". $result->{'crednum'};
275 my($class, %opt) = @_;
276 return _shared_secret_error() unless _check_shared_secret($opt{secret});
278 # when github pull request #24 is merged,
279 # will have to change over to default reasonnum like credit
280 # but until then, this will do
281 $opt{'reason'} ||= 'API refund';
283 #less "raw" than this? we are the backoffice API, and aren't worried
284 # about version migration ala cust_main/cust_location here
285 my $cust_refund = new FS::cust_refund { %opt };
286 my $error = $cust_refund->insert;
287 return { 'error' => $error,
288 'refundnum' => $cust_refund->refundnum,
292 # pass the phone number ( from svc_phone )
293 sub insert_refund_phonenum {
294 my($class, %opt) = @_;
295 $class->_by_phonenum('insert_refund', %opt);
300 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
301 # figure out how to trigger something when those things change
303 # long-term: package changes?
305 =item new_customer OPTION => VALUE, ...
307 Creates a new customer. Takes a list of keys and values as parameters with the
318 first name (required)
326 (not typically collected; mostly used for ACH transactions)
332 =item address1 (required)
336 =item city (required)
344 =item state (required)
366 Currently used for third party tax vendor lookups
370 Used for determining FCC 477 reporting
374 Used for determining FCC 477 reporting
390 Optional shipping address fields. If sending an optional shipping address,
391 ship_address1, ship_city, ship_state and ship_zip are required.
411 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),
413 Set to 1 to enable postal invoicing
415 =item referral_custnum
417 Referring customer number
429 Agent specific customer number
431 =item referral_custnum
433 Referring customer number
439 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
440 # but approaching this from a clean start / back-office perspective
441 # i.e. no package/service, no immediate credit card run, etc.
444 my( $class, %opt ) = @_;
445 return _shared_secret_error() unless _check_shared_secret($opt{secret});
447 #default agentnum like signup_server-default_agentnum?
448 #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
450 #same for refnum like signup_server-default_refnum
451 $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
453 FS::cust_main->API_insert( %opt );
456 =item update_customer
458 Updates an existing customer. Passing an empty value clears that field, while
459 NOT passing that key/value at all leaves it alone. Takes a list of keys and
460 values as parameters with the following keys:
466 API Secret (required)
470 Customer number (required)
526 Comma-separated list of email addresses for email invoices. The special value
527 'POST' is used to designate postal invoicing (it may be specified alone or in
528 addition to email addresses),
530 Set to 1 to enable postal invoicing
532 =item referral_custnum
534 Referring customer number
548 sub update_customer {
549 my( $class, %opt ) = @_;
550 return _shared_secret_error() unless _check_shared_secret($opt{secret});
552 FS::cust_main->API_update( %opt );
555 =item customer_info OPTION => VALUE, ...
557 Returns general customer information. Takes a list of keys and values as
558 parameters with the following keys: custnum, secret
562 use Frontier::Client;
565 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
568 my $xmlrpc = new Frontier::Client url=>$url;
570 my $result = $xmlrpc->call( 'FS.API.customer_info',
571 'secret' => 'sharingiscaring',
575 print Dumper($result);
580 my( $class, %opt ) = @_;
581 return _shared_secret_error() unless _check_shared_secret($opt{secret});
583 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
584 or return { 'error' => 'Unknown custnum' };
586 $cust_main->API_getinfo;
589 =item customer_list_svcs OPTION => VALUE, ...
591 Returns customer service information. Takes a list of keys and values as
592 parameters with the following keys: custnum, secret
596 use Frontier::Client;
599 my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
602 my $xmlrpc = new Frontier::Client url=>$url;
604 my $result = $xmlrpc->call( 'FS.API.customer_list_svcs',
605 'secret' => 'sharingiscaring',
609 print Dumper($result);
611 foreach my $cust_svc ( @{ $result->{'cust_svc'} } ) {
612 #print $cust_svc->{mac_addr}."\n" if exists $cust_svc->{mac_addr};
613 print $cust_svc->{circuit_id}."\n" if exists $cust_svc->{circuit_id};
618 sub customer_list_svcs {
619 my( $class, %opt ) = @_;
620 return _shared_secret_error() unless _check_shared_secret($opt{secret});
622 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
623 or return { 'error' => 'Unknown custnum' };
625 #$cust_main->API_list_svcs;
627 #false laziness w/ClientAPI/list_svcs
630 #my @cust_pkg_usage = ();
631 #foreach my $cust_pkg ( $p->{'ncancelled'}
632 # ? $cust_main->ncancelled_pkgs
633 # : $cust_main->unsuspended_pkgs ) {
634 foreach my $cust_pkg ( $cust_main->all_pkgs ) {
635 #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
636 push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
637 #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
641 'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
648 Returns location specific information for the customer. Takes a list of keys
649 and values as paramters with the following keys: custnum, secret
653 #I also monitor for changes to the additional locations that are applied to
654 # packages, and would like for those to be exportable as well. basically the
655 # location data passed with the custnum.
658 my( $class, %opt ) = @_;
659 return _shared_secret_error() unless _check_shared_secret($opt{secret});
661 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
665 'locations' => [ map $_->hashref, @cust_location ],
671 =item list_customer_packages OPTION => VALUE, ...
673 Lists all customer packages.
689 my $result = FS::API->list_packages(
690 'secret' => 'sharingiscaring',
691 'custnum' => custnum,
694 if ( $result->{'error'} ) {
695 die $result->{'error'};
697 # list packages returns an array of hashes for packages ordered by custnum and pkgnum.
698 print Dumper($result->{'pkgs'});
703 sub list_customer_packages {
704 my( $class, %opt ) = @_;
705 return _shared_secret_error() unless _check_shared_secret($opt{secret});
707 my $sql_query = FS::cust_pkg->search({ 'custnum' => $opt{custnum}, });
709 $sql_query->{order_by} = 'ORDER BY custnum, pkgnum';
711 my @packages = qsearch($sql_query)
712 or return { 'error' => 'No packages' };
715 'packages' => [ map $_->hashref, @packages ],
721 =item package_status OPTION => VALUE, ...
739 my $result = FS::API->package_status(
740 'secret' => 'sharingiscaring',
744 if ( $result->{'error'} ) {
745 die $result->{'error'};
747 # package status returns a hash with the status for a package.
748 print Dumper($result->{'status'});
754 my( $class, %opt ) = @_;
755 return _shared_secret_error() unless _check_shared_secret($opt{secret});
757 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{pkgnum} } )
758 or return { 'error' => 'No packages' };
761 'status' => $cust_pkg->status,
767 =item order_package OPTION => VALUE, ...
769 Orders a new customer package. Takes a list of keys and values as paramaters
770 with the following keys:
804 Including this implements per-customer custom pricing for this package, overriding package definition pricing
808 Including this implements per-customer custom pricing for this package, overriding package definition pricing
810 =item invoice_details
812 A single string for just one detail line, or an array reference of one or more
820 my( $class, %opt ) = @_;
822 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
823 or return { 'error' => 'Unknown custnum' };
825 #some conceptual false laziness w/cust_pkg/Import.pm
827 my $cust_pkg = new FS::cust_pkg {
828 'pkgpart' => $opt{'pkgpart'},
829 'quantity' => $opt{'quantity'} || 1,
832 #start_date and contract_end
833 foreach my $date_field (qw( start_date contract_end )) {
834 if ( $opt{$date_field} =~ /^(\d+)$/ ) {
835 $cust_pkg->$date_field( $opt{$date_field} );
836 } elsif ( $opt{$date_field} ) {
837 $cust_pkg->$date_field( str2time( $opt{$date_field} ) );
841 #especially this part for custom pkg price
842 # (false laziness w/cust_pkg/Import.pm)
843 my $s = $opt{'setup_fee'};
844 my $r = $opt{'recur_fee'};
845 my $part_pkg = $cust_pkg->part_pkg;
846 if ( ( length($s) && $s != $part_pkg->option('setup_fee') )
847 or ( length($r) && $r != $part_pkg->option('recur_fee') )
851 local($FS::part_pkg::skip_pkg_svc_hack) = 1;
853 my $custom_part_pkg = $part_pkg->clone;
854 $custom_part_pkg->disabled('Y');
855 my %options = $part_pkg->options;
856 $options{'setup_fee'} = $s if length($s);
857 $options{'recur_fee'} = $r if length($r);
858 my $error = $custom_part_pkg->insert( options=>\%options );
859 return ( 'error' => "error customizing package: $error" ) if $error;
861 #not ->pkg_svc, we want to ignore links and clone the actual package def
862 foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
863 my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
864 $c_pkg_svc->pkgsvcnum('');
865 $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
866 my $error = $c_pkg_svc->insert;
867 return "error customizing package: $error" if $error;
870 $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
874 my %order_pkg = ( 'cust_pkg' => $cust_pkg );
876 my @loc_fields = qw( address1 address2 city county state zip country );
877 if ( grep length($opt{$_}), @loc_fields ) {
878 $order_pkg{'cust_location'} = new FS::cust_location {
879 map { $_ => $opt{$_} } @loc_fields, 'custnum'
883 $order_pkg{'invoice_details'} = $opt{'invoice_details'}
884 if $opt{'invoice_details'};
886 my $error = $cust_main->order_pkg( %order_pkg );
889 return { 'error' => $error,
893 # return { 'error' => '',
894 # #cust_main->order_pkg doesn't actually have a way to return pkgnum
895 # #'pkgnum' => $pkgnum,
901 =item change_package_location
903 Updates package location. Takes a list of keys and values
904 as parameters with the following keys:
910 locationnum - pass this, or the following keys (don't pass both)
942 On error, returns a hashref with an 'error' key.
943 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
944 containing the new values.
948 sub change_package_location {
951 return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
953 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
954 or return { 'error' => 'Unknown pkgnum' };
958 foreach my $field ( qw(
976 $changeopt{$field} = $opt{$field} if $opt{$field};
979 $cust_pkg->API_change(%changeopt);
982 =item bill_now OPTION => VALUE, ...
984 Bills a single customer now, in the same fashion as the "Bill now" link in the
987 Returns a hash reference with a single key, 'error'. If there is an error,
988 the value contains the error, otherwise it is empty. Takes a list of keys and
989 values as parameters with the following keys:
995 API Secret (required)
999 Customer number (required)
1006 my( $class, %opt ) = @_;
1007 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1009 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
1010 or return { 'error' => 'Unknown custnum' };
1012 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
1014 'check_freq' =>'1d',
1017 return { 'error' => $error,
1023 #next.. Delete Advertising sources?
1025 =item list_advertising_sources OPTION => VALUE, ...
1027 Lists all advertising sources.
1039 my $result = FS::API->list_advertising_sources(
1040 'secret' => 'sharingiscaring',
1043 if ( $result->{'error'} ) {
1044 die $result->{'error'};
1046 # list advertising sources returns an array of hashes for sources.
1047 print Dumper($result->{'sources'});
1052 #list_advertising_sources
1053 sub list_advertising_sources {
1054 my( $class, %opt ) = @_;
1055 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1057 my @sources = qsearch('part_referral', {}, '', "")
1058 or return { 'error' => 'No referrals' };
1061 'sources' => [ map $_->hashref, @sources ],
1067 =item add_advertising_source OPTION => VALUE, ...
1069 Add a new advertising source.
1083 Referral disabled, Y for disabled or nothing for enabled
1091 External referral ID
1097 my $result = FS::API->add_advertising_source(
1098 'secret' => 'sharingiscaring',
1099 'referral' => 'test referral',
1103 'agentnum' => '2', #agent id number
1104 'title' => 'test title',
1107 if ( $result->{'error'} ) {
1108 die $result->{'error'};
1110 # add_advertising_source returns new source upon success.
1111 print Dumper($result);
1116 #add_advertising_source
1117 sub add_advertising_source {
1118 my( $class, %opt ) = @_;
1119 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1121 use FS::part_referral;
1123 my $new_source = $opt{source};
1125 my $source = new FS::part_referral $new_source;
1127 my $error = $source->insert;
1129 my $return = {$source->hash};
1130 $return = { 'error' => $error, } if $error;
1135 =item edit_advertising_source OPTION => VALUE, ...
1137 Edit a advertising source.
1147 Referral number to edit
1151 hash of edited source fields.
1161 Referral disabled, Y for disabled or nothing for enabled
1169 External referral ID
1177 my $result = FS::API->edit_advertising_source(
1178 'secret' => 'sharingiscaring',
1179 'refnum' => '4', # referral number to edit
1182 'referral' => 'test referral',
1184 'agentnum' => '2', #agent id number
1185 'title' => 'test title',
1189 if ( $result->{'error'} ) {
1190 die $result->{'error'};
1192 # edit_advertising_source returns updated source upon success.
1193 print Dumper($result);
1198 #edit_advertising_source
1199 sub edit_advertising_source {
1200 my( $class, %opt ) = @_;
1201 return _shared_secret_error() unless _check_shared_secret($opt{secret});
1203 use FS::part_referral;
1205 my $refnum = $opt{refnum};
1206 my $source = $opt{source};
1208 my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1209 my $new = new FS::part_referral { $old->hash };
1211 foreach my $key (keys %$source) {
1212 $new->$key($source->{$key});
1215 my $error = $new->replace;
1217 my $return = {$new->hash};
1218 $return = { 'error' => $error, } if $error;
1224 =item email_optout OPTION => VALUE, ...
1226 Each e-mail address, or L<FS::cust_contact> record, has two opt-in flags:
1227 message_dest: recieve non-invoicing messages, and invoice_dest: recieve
1230 Use this API call to remove opt-in flags for an e-mail address
1238 =item disable_message_dest
1241 Set this parameter as 0 in your API call to leave the message_dest flag as is
1243 =item disable_invoice_dest
1246 Set this parameter as 0 in your API call to leave the invoice_dest flag as is
1253 my ($class, %opt) = @_;
1255 return _shared_secret_error()
1256 unless _check_shared_secret($opt{secret});
1258 return {error => 'No e-mail address specified'}
1259 unless $opt{address} && $opt{address} =~ /\@/;
1261 $opt{disable_message_dest} ||= 1;
1262 $opt{disable_invoice_dest} ||= 1;
1264 my $address = FS::Record::dbh->quote($opt{address});
1266 for my $cust_contact (
1267 FS::Record::qsearch({
1268 table => 'cust_contact',
1269 select => 'cust_contact.*',
1270 addl_from => 'LEFT JOIN contact_email USING (contactnum)',
1271 extra_sql => "WHERE contact_email.emailaddress = $address",
1274 $cust_contact->set(invoice_dest => '') if $opt{disable_invoice_dest};
1275 $cust_contact->set(message_dest => '') if $opt{disable_message_dest};
1277 my $error = $cust_contact->replace();
1278 return {error => $error} if $error;
1285 # helper subroutines
1288 sub _check_shared_secret {
1289 shift eq FS::Conf->new->config('api_shared_secret');
1292 sub _shared_secret_error {
1293 return { 'error' => 'Incorrect shared secret' };