5 use FS::Record qw( qsearch qsearchs );
15 FS::API - Freeside backend API
23 This module implements a backend API for advanced back-office integration.
25 In contrast to the self-service API, which authenticates an end-user and offers
26 functionality to that end user, the backend API performs a simple shared-secret
27 authentication and offers full, administrator functionality, enabling
28 integration with other back-office systems. Only access this API from a secure
29 network from other backoffice machines. DON'T use this API to create customer
32 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
33 the port by default, only allow access from back-office servers with the same
34 security precations as the Freeside server, and encrypt the communication
35 channel (for example, with an SSH tunnel or VPN) rather than accessing it
42 =item insert_payment OPTION => VALUE, ...
44 Adds a new payment to a customers account. Takes a list of keys and values as
45 paramters with the following keys:
67 Option date for payment
77 my $result = FS::API->insert_payment(
78 'secret' => 'sharingiscaring',
84 '_date' => 1397977200, #UNIX timestamp
85 'order_number' => '12345',
88 if ( $result->{'error'} ) {
89 die $result->{'error'};
92 print "paynum ". $result->{'paynum'};
99 my($class, %opt) = @_;
100 return _shared_secret_error() unless _check_shared_secret($opt{secret});
102 #less "raw" than this? we are the backoffice API, and aren't worried
103 # about version migration ala cust_main/cust_location here
104 my $cust_pay = new FS::cust_pay { %opt };
105 my $error = $cust_pay->insert( 'manual'=>1 );
106 return { 'error' => $error,
107 'paynum' => $cust_pay->paynum,
111 # pass the phone number ( from svc_phone )
112 sub insert_payment_phonenum {
113 my($class, %opt) = @_;
114 $class->_by_phonenum('insert_payment', %opt);
118 my($class, $method, %opt) = @_;
119 return _shared_secret_error() unless _check_shared_secret($opt{secret});
121 my $phonenum = delete $opt{'phonenum'};
123 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
124 or return { 'error' => 'Unknown phonenum' };
126 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
127 or return { 'error' => 'Unlinked phonenum' };
129 $opt{'custnum'} = $cust_pkg->custnum;
131 $class->$method(%opt);
134 =item insert_credit OPTION => VALUE, ...
136 Adds a a credit to a customers account. Takes a list of keys and values as
137 parameters with the following keys
155 The date the credit will be posted
161 my $result = FS::API->insert_credit(
162 'secret' => 'sharingiscaring',
167 '_date' => 1397977200, #UNIX timestamp
170 if ( $result->{'error'} ) {
171 die $result->{'error'};
174 print "crednum ". $result->{'crednum'};
181 my($class, %opt) = @_;
182 return _shared_secret_error() unless _check_shared_secret($opt{secret});
184 $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
186 #less "raw" than this? we are the backoffice API, and aren't worried
187 # about version migration ala cust_main/cust_location here
188 my $cust_credit = new FS::cust_credit { %opt };
189 my $error = $cust_credit->insert;
190 return { 'error' => $error,
191 'crednum' => $cust_credit->crednum,
195 # pass the phone number ( from svc_phone )
196 sub insert_credit_phonenum {
197 my($class, %opt) = @_;
198 $class->_by_phonenum('insert_credit', %opt);
201 =item apply_payments_and_credits
203 Applies payments and credits for this customer. Takes a list of keys and
204 values as parameter with the following keys:
220 #apply payments and credits
221 sub apply_payments_and_credits {
222 my($class, %opt) = @_;
223 return _shared_secret_error() unless _check_shared_secret($opt{secret});
225 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
226 or return { 'error' => 'Unknown custnum' };
228 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
229 return { 'error' => $error, };
232 =item insert_refund OPTION => VALUE, ...
234 Adds a a credit to a customers account. Takes a list of keys and values as
235 parmeters with the following keys: custnum, payby, refund
239 my $result = FS::API->insert_refund(
240 'secret' => 'sharingiscaring',
246 '_date' => 1397977200, #UNIX timestamp
249 if ( $result->{'error'} ) {
250 die $result->{'error'};
253 print "refundnum ". $result->{'crednum'};
260 my($class, %opt) = @_;
261 return _shared_secret_error() unless _check_shared_secret($opt{secret});
263 # when github pull request #24 is merged,
264 # will have to change over to default reasonnum like credit
265 # but until then, this will do
266 $opt{'reason'} ||= 'API refund';
268 #less "raw" than this? we are the backoffice API, and aren't worried
269 # about version migration ala cust_main/cust_location here
270 my $cust_refund = new FS::cust_refund { %opt };
271 my $error = $cust_refund->insert;
272 return { 'error' => $error,
273 'refundnum' => $cust_refund->refundnum,
277 # pass the phone number ( from svc_phone )
278 sub insert_refund_phonenum {
279 my($class, %opt) = @_;
280 $class->_by_phonenum('insert_refund', %opt);
285 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
286 # figure out how to trigger something when those things change
288 # long-term: package changes?
290 =item new_customer OPTION => VALUE, ...
292 Creates a new customer. Takes a list of keys and values as parameters with the
303 first name (required)
311 (not typically collected; mostly used for ACH transactions)
317 =item address1 (required)
321 =item city (required)
329 =item state (required)
351 Currently used for third party tax vendor lookups
355 Used for determining FCC 477 reporting
359 Used for determining FCC 477 reporting
379 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),
381 Set to 1 to enable postal invoicing
383 =item referral_custnum
385 Referring customer number
397 Agent specific customer number
399 =item referral_custnum
401 Referring customer number
407 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
408 # but approaching this from a clean start / back-office perspective
409 # i.e. no package/service, no immediate credit card run, etc.
412 my( $class, %opt ) = @_;
413 return _shared_secret_error() unless _check_shared_secret($opt{secret});
415 #default agentnum like signup_server-default_agentnum?
416 #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
418 #same for refnum like signup_server-default_refnum
419 $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
421 $class->API_insert( %opt );
424 =item update_customer
426 Updates an existing customer. Passing an empty value clears that field, while
427 NOT passing that key/value at all leaves it alone. Takes a list of keys and
428 values as parameters with the following keys:
434 API Secret (required)
438 Customer number (required)
494 Comma-separated list of email addresses for email invoices. The special value
495 'POST' is used to designate postal invoicing (it may be specified alone or in
496 addition to email addresses),
498 Set to 1 to enable postal invoicing
500 =item referral_custnum
502 Referring customer number
516 sub update_customer {
517 my( $class, %opt ) = @_;
518 return _shared_secret_error() unless _check_shared_secret($opt{secret});
520 FS::cust_main->API_update( %opt );
523 =item customer_info OPTION => VALUE, ...
525 Returns general customer information. Takes a list of keys and values as
526 parameters with the following keys: custnum, secret
531 my( $class, %opt ) = @_;
532 return _shared_secret_error() unless _check_shared_secret($opt{secret});
534 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
535 or return { 'error' => 'Unknown custnum' };
537 $cust_main->API_getinfo;
540 =item customer_list_svcs OPTION => VALUE, ...
542 Returns customer service information. Takes a list of keys and values as
543 parameters with the following keys: custnum, secret
547 sub customer_list_svcs {
548 my( $class, %opt ) = @_;
549 return _shared_secret_error() unless _check_shared_secret($opt{secret});
551 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
552 or return { 'error' => 'Unknown custnum' };
554 #$cust_main->API_list_svcs;
556 #false laziness w/ClientAPI/list_svcs
559 #my @cust_pkg_usage = ();
560 #foreach my $cust_pkg ( $p->{'ncancelled'}
561 # ? $cust_main->ncancelled_pkgs
562 # : $cust_main->unsuspended_pkgs ) {
563 foreach my $cust_pkg ( $cust_main->all_pkgs ) {
564 #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
565 push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
566 #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
570 'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
577 Returns location specific information for the customer. Takes a list of keys
578 and values as paramters with the following keys: custnum, secret
582 #I also monitor for changes to the additional locations that are applied to
583 # packages, and would like for those to be exportable as well. basically the
584 # location data passed with the custnum.
587 my( $class, %opt ) = @_;
588 return _shared_secret_error() unless _check_shared_secret($opt{secret});
590 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
594 'locations' => [ map $_->hashref, @cust_location ],
600 =item change_package_location
602 Updates package location. Takes a list of keys and values
603 as paramters with the following keys:
609 locationnum - pass this, or the following keys (don't pass both)
641 On error, returns a hashref with an 'error' key.
642 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
643 containing the new values.
647 sub change_package_location {
650 return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
652 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
653 or return { 'error' => 'Unknown pkgnum' };
657 foreach my $field ( qw(
675 $changeopt{$field} = $opt{$field} if $opt{$field};
678 $cust_pkg->API_change(%changeopt);
681 =item bill_now OPTION => VALUE, ...
683 Bills a single customer now, in the same fashion as the "Bill now" link in the
686 Returns a hash reference with a single key, 'error'. If there is an error,
687 the value contains the error, otherwise it is empty. Takes a list of keys and
688 values as parameters with the following keys:
694 API Secret (required)
698 Customer number (required)
705 my( $class, %opt ) = @_;
706 return _shared_secret_error() unless _check_shared_secret($opt{secret});
708 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
709 or return { 'error' => 'Unknown custnum' };
711 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
716 return { 'error' => $error,
722 #next.. Delete Advertising sources?
724 =item list_advertising_sources OPTION => VALUE, ...
726 Lists all advertising sources.
738 my $result = FS::API->list_advertising_sources(
739 'secret' => 'sharingiscaring',
742 if ( $result->{'error'} ) {
743 die $result->{'error'};
745 # list advertising sources returns an array of hashes for sources.
746 print Dumper($result->{'sources'});
751 #list_advertising_sources
752 sub list_advertising_sources {
753 my( $class, %opt ) = @_;
754 return _shared_secret_error() unless _check_shared_secret($opt{secret});
756 my @sources = qsearch('part_referral', {}, '', "")
757 or return { 'error' => 'No referrals' };
760 'sources' => [ map $_->hashref, @sources ],
766 =item add_advertising_source OPTION => VALUE, ...
768 Add a new advertising source.
782 Referral disabled, Y for disabled or nothing for enabled
796 my $result = FS::API->add_advertising_source(
797 'secret' => 'sharingiscaring',
798 'referral' => 'test referral',
802 'agentnum' => '2', #agent id number
803 'title' => 'test title',
806 if ( $result->{'error'} ) {
807 die $result->{'error'};
809 # add_advertising_source returns new source upon success.
810 print Dumper($result);
815 #add_advertising_source
816 sub add_advertising_source {
817 my( $class, %opt ) = @_;
818 return _shared_secret_error() unless _check_shared_secret($opt{secret});
820 use FS::part_referral;
822 my $new_source = $opt{source};
824 my $source = new FS::part_referral $new_source;
826 my $error = $source->insert;
828 my $return = {$source->hash};
829 $return = { 'error' => $error, } if $error;
834 =item edit_advertising_source OPTION => VALUE, ...
836 Edit a advertising source.
846 Referral number to edit
850 hash of edited source fields.
860 Referral disabled, Y for disabled or nothing for enabled
876 my $result = FS::API->edit_advertising_source(
877 'secret' => 'sharingiscaring',
878 'refnum' => '4', # referral number to edit
881 'referral' => 'test referral',
883 'agentnum' => '2', #agent id number
884 'title' => 'test title',
888 if ( $result->{'error'} ) {
889 die $result->{'error'};
891 # edit_advertising_source returns updated source upon success.
892 print Dumper($result);
897 #edit_advertising_source
898 sub edit_advertising_source {
899 my( $class, %opt ) = @_;
900 return _shared_secret_error() unless _check_shared_secret($opt{secret});
902 use FS::part_referral;
904 my $refnum = $opt{refnum};
905 my $source = $opt{source};
907 my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
908 my $new = new FS::part_referral { $old->hash };
910 foreach my $key (keys %$source) {
911 $new->$key($source->{$key});
914 my $error = $new->replace;
916 my $return = {$new->hash};
917 $return = { 'error' => $error, } if $error;
927 sub _check_shared_secret {
928 shift eq FS::Conf->new->config('api_shared_secret');
931 sub _shared_secret_error {
932 return { 'error' => 'Incorrect shared secret' };