4 use FS::Record qw( qsearch qsearchs );
13 FS::API - Freeside backend API
21 This module implements a backend API for advanced back-office integration.
23 In contrast to the self-service API, which authenticates an end-user and offers
24 functionality to that end user, the backend API performs a simple shared-secret
25 authentication and offers full, administrator functionality, enabling
26 integration with other back-office systems. Only access this API from a secure
27 network from other backoffice machines. DON'T use this API to create customer
30 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
31 the port by default, only allow access from back-office servers with the same
32 security precations as the Freeside server, and encrypt the communication
33 channel (for example, with an SSH tunnel or VPN) rather than accessing it
40 =item insert_payment OPTION => VALUE, ...
42 Adds a new payment to a customers account. Takes a list of keys and values as
43 paramters with the following keys:
65 Option date for payment
71 my $result = FS::API->insert_payment(
72 'secret' => 'sharingiscaring',
78 '_date' => 1397977200, #UNIX timestamp
81 if ( $result->{'error'} ) {
82 die $result->{'error'};
85 print "paynum ". $result->{'paynum'};
92 my($class, %opt) = @_;
93 my $conf = new FS::Conf;
94 return { 'error' => 'Incorrect shared secret' }
95 unless $opt{secret} eq $conf->config('api_shared_secret');
97 #less "raw" than this? we are the backoffice API, and aren't worried
98 # about version migration ala cust_main/cust_location here
99 my $cust_pay = new FS::cust_pay { %opt };
100 my $error = $cust_pay->insert( 'manual'=>1 );
101 return { 'error' => $error,
102 'paynum' => $cust_pay->paynum,
106 # pass the phone number ( from svc_phone )
107 sub insert_payment_phonenum {
108 my($class, %opt) = @_;
109 my $conf = new FS::Conf;
110 return { 'error' => 'Incorrect shared secret' }
111 unless $opt{secret} eq $conf->config('api_shared_secret');
113 $class->_by_phonenum('insert_payment', %opt);
118 my($class, $method, %opt) = @_;
119 my $conf = new FS::Conf;
120 return { 'error' => 'Incorrect shared secret' }
121 unless $opt{secret} eq $conf->config('api_shared_secret');
123 my $phonenum = delete $opt{'phonenum'};
125 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
126 or return { 'error' => 'Unknown phonenum' };
128 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
129 or return { 'error' => 'Unlinked phonenum' };
131 $opt{'custnum'} = $cust_pkg->custnum;
133 $class->$method(%opt);
137 =item insert_credit OPTION => VALUE, ...
139 Adds a a credit to a customers account. Takes a list of keys and values as
140 parameters with the following keys
158 The date the credit will be posted
164 my $result = FS::API->insert_credit(
165 'secret' => 'sharingiscaring',
170 '_date' => 1397977200, #UNIX timestamp
173 if ( $result->{'error'} ) {
174 die $result->{'error'};
177 print "crednum ". $result->{'crednum'};
184 my($class, %opt) = @_;
185 my $conf = new FS::Conf;
186 return { 'error' => 'Incorrect shared secret' }
187 unless $opt{secret} eq $conf->config('api_shared_secret');
189 $opt{'reasonnum'} ||= $conf->config('api_credit_reason');
191 #less "raw" than this? we are the backoffice API, and aren't worried
192 # about version migration ala cust_main/cust_location here
193 my $cust_credit = new FS::cust_credit { %opt };
194 my $error = $cust_credit->insert;
195 return { 'error' => $error,
196 'crednum' => $cust_credit->crednum,
200 # pass the phone number ( from svc_phone )
201 sub insert_credit_phonenum {
202 my($class, %opt) = @_;
203 my $conf = new FS::Conf;
204 return { 'error' => 'Incorrect shared secret' }
205 unless $opt{secret} eq $conf->config('api_shared_secret');
207 $class->_by_phonenum('insert_credit', %opt);
211 =item insert_refund OPTION => VALUE, ...
213 Adds a a credit to a customers account. Takes a list of keys and values as
214 parmeters with the following keys: custnum, payby, refund
218 my $result = FS::API->insert_refund(
219 'secret' => 'sharingiscaring',
225 '_date' => 1397977200, #UNIX timestamp
228 if ( $result->{'error'} ) {
229 die $result->{'error'};
232 print "refundnum ". $result->{'crednum'};
239 my($class, %opt) = @_;
240 my $conf = new FS::Conf;
241 return { 'error' => 'Incorrect shared secret' }
242 unless $opt{secret} eq $conf->config('api_shared_secret');
244 # when github pull request #24 is merged,
245 # will have to change over to default reasonnum like credit
246 # but until then, this will do
247 $opt{'reason'} ||= 'API refund';
249 #less "raw" than this? we are the backoffice API, and aren't worried
250 # about version migration ala cust_main/cust_location here
251 my $cust_refund = new FS::cust_refund { %opt };
252 my $error = $cust_refund->insert;
253 return { 'error' => $error,
254 'refundnum' => $cust_refund->refundnum,
258 # pass the phone number ( from svc_phone )
259 sub insert_refund_phonenum {
260 my($class, %opt) = @_;
261 my $conf = new FS::Conf;
262 return { 'error' => 'Incorrect shared secret' }
263 unless $opt{secret} eq $conf->config('api_shared_secret');
265 $class->_by_phonenum('insert_refund', %opt);
271 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
272 # figure out how to trigger something when those things change
274 # long-term: package changes?
276 =item new_customer OPTION => VALUE, ...
278 Creates a new customer. Takes a list of keys and values as parameters with the
289 first name (required)
297 (not typically collected; mostly used for ACH transactions)
303 =item address1 (required)
307 =item city (required)
315 =item state (required)
337 Currently used for third party tax vendor lookups
341 Used for determining FCC 477 reporting
345 Used for determining FCC 477 reporting
365 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),
367 Set to 1 to enable postal invoicing
371 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
375 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
379 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
383 Expiration date for CARD/DCRD
387 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
389 =item referral_custnum
391 Referring customer number
403 Agent specific customer number
405 =item referral_custnum
407 Referring customer number
413 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
414 # but approaching this from a clean start / back-office perspective
415 # i.e. no package/service, no immediate credit card run, etc.
418 my( $class, %opt ) = @_;
419 my $conf = new FS::Conf;
420 return { 'error' => 'Incorrect shared secret' }
421 unless $opt{secret} eq $conf->config('api_shared_secret');
423 #default agentnum like signup_server-default_agentnum?
425 #same for refnum like signup_server-default_refnum
427 my $cust_main = new FS::cust_main ( {
428 'agentnum' => $agentnum,
429 'refnum' => $opt{refnum}
430 || $conf->config('signup_server-default_refnum'),
432 'tagnum' => [ FS::part_tag->default_tags ],
434 map { $_ => $opt{$_} } qw(
435 agentnum salesnum refnum agent_custid referral_custnum
437 daytime night fax mobile
438 payby payinfo paydate paycvv payname
443 my @invoicing_list = $opt{'invoicing_list'}
444 ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
446 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
448 my ($bill_hash, $ship_hash);
449 foreach my $f (FS::cust_main->location_fields) {
450 # avoid having to change this in front-end code
451 $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
452 $ship_hash->{$f} = $opt{"ship_$f"};
455 my $bill_location = FS::cust_location->new($bill_hash);
457 # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
458 # so is there a ship address, and if so, is it different from the billing
460 if ( length($ship_hash->{address1}) > 0 and
461 grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
464 $ship_location = FS::cust_location->new( $ship_hash );
467 $ship_location = $bill_location;
470 $cust_main->set('bill_location' => $bill_location);
471 $cust_main->set('ship_location' => $ship_location);
473 $error = $cust_main->insert( {}, \@invoicing_list );
474 return { 'error' => $error } if $error;
476 return { 'error' => '',
477 'custnum' => $cust_main->custnum,
482 =item update_customer
484 Updates an existing customer. Passing an empty value clears that field, while
485 NOT passing that key/value at all leaves it alone. Takes a list of keys and
486 values as parameters with the following keys:
492 API Secret (required)
496 Customer number (required)
552 Comma-separated list of email addresses for email invoices. The special value
553 'POST' is used to designate postal invoicing (it may be specified alone or in
554 addition to email addresses)
558 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
562 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid
563 +"pin" for PREPAY, purchase order number for BILL
567 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
571 Expiration date for CARD/DCRD
575 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
577 =item referral_custnum
579 Referring customer number
593 sub update_customer {
595 my( $class, %opt ) = @_;
597 my $conf = new FS::Conf;
598 return { 'error' => 'Incorrect shared secret' }
599 unless $opt{secret} eq $conf->config('api_shared_secret');
602 my $custnum = $opt{'custnum'}
603 or return { 'error' => "no customer record" };
605 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
606 or return { 'error' => "unknown custnum $custnum" };
608 my $new = new FS::cust_main { $cust_main->hash };
610 $new->set( $_ => $opt{$_} )
611 foreach grep { exists $opt{$_} } qw(
612 agentnum salesnum refnum agent_custid referral_custnum
614 daytime night fax mobile
615 payby payinfo paydate paycvv payname
619 if ( exists $opt{'invoicing_list'} || exists $opt{'postal_invoicing'} ) {
620 @invoicing_list = split( /\s*\,\s*/, $opt{'invoicing_list'} );
621 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
623 @invoicing_list = $cust_main->invoicing_list;
626 if ( exists( $opt{'address1'} ) ) {
627 my $bill_location = FS::cust_location->new({
628 map { $_ => $opt{$_} } @location_editable_fields
630 $bill_location->set('custnum' => $custnum);
631 my $error = $bill_location->find_or_insert;
632 die $error if $error;
634 # if this is unchanged from before, cust_main::replace will ignore it
635 $new->set('bill_location' => $bill_location);
638 if ( exists($opt{'ship_address1'}) && length($opt{"ship_address1"}) > 0 ) {
639 my $ship_location = FS::cust_location->new({
640 map { $_ => $opt{"ship_$_"} } @location_editable_fields
643 $ship_location->set('custnum' => $custnum);
644 my $error = $ship_location->find_or_insert;
645 die $error if $error;
647 $new->set('ship_location' => $ship_location);
649 } elsif (exists($opt{'ship_address1'} ) && !grep { length($opt{"ship_$_"}) } @location_editable_fields ) {
650 my $ship_location = $new->bill_location;
651 $new->set('ship_location' => $ship_location);
654 my $error = $new->replace( $cust_main, \@invoicing_list );
655 return { 'error' => $error } if $error;
657 return { 'error' => '',
664 Returns general customer information. Takes a list of keys and values as
665 parameters with the following keys: custnum, secret
669 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
671 use vars qw( @cust_main_editable_fields @location_editable_fields );
672 @cust_main_editable_fields = qw(
673 first last company daytime night fax mobile
676 # payby payinfo payname paystart_month paystart_year payissue payip
677 # ss paytype paystate stateid stateid_state
678 @location_editable_fields = qw(
679 address1 address2 city county state zip country
683 my( $class, %opt ) = @_;
684 my $conf = new FS::Conf;
685 return { 'error' => 'Incorrect shared secret' }
686 unless $opt{secret} eq $conf->config('api_shared_secret');
688 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
689 or return { 'error' => 'Unknown custnum' };
693 'display_custnum' => $cust_main->display_custnum,
694 'name' => $cust_main->first. ' '. $cust_main->get('last'),
695 'balance' => $cust_main->balance,
696 'status' => $cust_main->status,
697 'statuscolor' => $cust_main->statuscolor,
700 $return{$_} = $cust_main->get($_)
701 foreach @cust_main_editable_fields;
703 for (@location_editable_fields) {
704 $return{$_} = $cust_main->bill_location->get($_)
705 if $cust_main->bill_locationnum;
706 $return{'ship_'.$_} = $cust_main->ship_location->get($_)
707 if $cust_main->ship_locationnum;
710 my @invoicing_list = $cust_main->invoicing_list;
711 $return{'invoicing_list'} =
712 join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
713 $return{'postal_invoicing'} =
714 0 < ( grep { $_ eq 'POST' } @invoicing_list );
716 #generally, the more useful data from the cust_main record the better.
717 # well, tell me what you want
726 Returns location specific information for the customer. Takes a list of keys
727 and values as paramters with the following keys: custnum, secret
731 #I also monitor for changes to the additional locations that are applied to
732 # packages, and would like for those to be exportable as well. basically the
733 # location data passed with the custnum.
736 my( $class, %opt ) = @_;
737 my $conf = new FS::Conf;
738 return { 'error' => 'Incorrect shared secret' }
739 unless $opt{secret} eq $conf->config('api_shared_secret');
741 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
745 'locations' => [ map $_->hashref, @cust_location ],
751 =item bill_now OPTION => VALUE, ...
753 Bills a single customer now, in the same fashion as the "Bill now" link in the
756 Returns a hash reference with a single key, 'error'. If there is an error,
757 the value contains the error, otherwise it is empty. Takes a list of keys and
758 values as parameters with the following keys:
764 API Secret (required)
768 Customer number (required)
775 my( $class, %opt ) = @_;
776 my $conf = new FS::Conf;
777 return { 'error' => 'Incorrect shared secret' }
778 unless $opt{secret} eq $conf->config('api_shared_secret');
780 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
781 or return { 'error' => 'Unknown custnum' };
783 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
788 return { 'error' => $error,
794 #Advertising sources?