4 use FS::Record qw( qsearch qsearchs );
14 FS::API - Freeside backend API
22 This module implements a backend API for advanced back-office integration.
24 In contrast to the self-service API, which authenticates an end-user and offers
25 functionality to that end user, the backend API performs a simple shared-secret
26 authentication and offers full, administrator functionality, enabling
27 integration with other back-office systems. Only access this API from a secure
28 network from other backoffice machines. DON'T use this API to create customer
31 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
32 the port by default, only allow access from back-office servers with the same
33 security precations as the Freeside server, and encrypt the communication
34 channel (for example, with an SSH tunnel or VPN) rather than accessing it
41 =item insert_payment OPTION => VALUE, ...
43 Adds a new payment to a customers account. Takes a list of keys and values as
44 paramters with the following keys:
66 Option date for payment
72 my $result = FS::API->insert_payment(
73 'secret' => 'sharingiscaring',
79 '_date' => 1397977200, #UNIX timestamp
82 if ( $result->{'error'} ) {
83 die $result->{'error'};
86 print "paynum ". $result->{'paynum'};
93 my($class, %opt) = @_;
94 return _shared_secret_error() unless _check_shared_secret($opt{secret});
96 #less "raw" than this? we are the backoffice API, and aren't worried
97 # about version migration ala cust_main/cust_location here
98 my $cust_pay = new FS::cust_pay { %opt };
99 my $error = $cust_pay->insert( 'manual'=>1 );
100 return { 'error' => $error,
101 'paynum' => $cust_pay->paynum,
105 # pass the phone number ( from svc_phone )
106 sub insert_payment_phonenum {
107 my($class, %opt) = @_;
108 $class->_by_phonenum('insert_payment', %opt);
112 my($class, $method, %opt) = @_;
113 return _shared_secret_error() unless _check_shared_secret($opt{secret});
115 my $phonenum = delete $opt{'phonenum'};
117 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
118 or return { 'error' => 'Unknown phonenum' };
120 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
121 or return { 'error' => 'Unlinked phonenum' };
123 $opt{'custnum'} = $cust_pkg->custnum;
125 $class->$method(%opt);
128 =item insert_credit OPTION => VALUE, ...
130 Adds a a credit to a customers account. Takes a list of keys and values as
131 parameters with the following keys
149 The date the credit will be posted
155 my $result = FS::API->insert_credit(
156 'secret' => 'sharingiscaring',
161 '_date' => 1397977200, #UNIX timestamp
164 if ( $result->{'error'} ) {
165 die $result->{'error'};
168 print "crednum ". $result->{'crednum'};
175 my($class, %opt) = @_;
176 return _shared_secret_error() unless _check_shared_secret($opt{secret});
178 $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
180 #less "raw" than this? we are the backoffice API, and aren't worried
181 # about version migration ala cust_main/cust_location here
182 my $cust_credit = new FS::cust_credit { %opt };
183 my $error = $cust_credit->insert;
184 return { 'error' => $error,
185 'crednum' => $cust_credit->crednum,
189 # pass the phone number ( from svc_phone )
190 sub insert_credit_phonenum {
191 my($class, %opt) = @_;
192 $class->_by_phonenum('insert_credit', %opt);
195 =item apply_payments_and_credits
197 Applies payments and credits for this customer. Takes a list of keys and
198 values as parameter with the following keys:
214 #apply payments and credits
215 sub apply_payments_and_credits {
216 my($class, %opt) = @_;
217 return _shared_secret_error() unless _check_shared_secret($opt{secret});
219 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
220 or return { 'error' => 'Unknown custnum' };
222 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
223 return { 'error' => $error, };
226 =item insert_refund OPTION => VALUE, ...
228 Adds a a credit to a customers account. Takes a list of keys and values as
229 parmeters with the following keys: custnum, payby, refund
233 my $result = FS::API->insert_refund(
234 'secret' => 'sharingiscaring',
240 '_date' => 1397977200, #UNIX timestamp
243 if ( $result->{'error'} ) {
244 die $result->{'error'};
247 print "refundnum ". $result->{'crednum'};
254 my($class, %opt) = @_;
255 return _shared_secret_error() unless _check_shared_secret($opt{secret});
257 # when github pull request #24 is merged,
258 # will have to change over to default reasonnum like credit
259 # but until then, this will do
260 $opt{'reason'} ||= 'API refund';
262 #less "raw" than this? we are the backoffice API, and aren't worried
263 # about version migration ala cust_main/cust_location here
264 my $cust_refund = new FS::cust_refund { %opt };
265 my $error = $cust_refund->insert;
266 return { 'error' => $error,
267 'refundnum' => $cust_refund->refundnum,
271 # pass the phone number ( from svc_phone )
272 sub insert_refund_phonenum {
273 my($class, %opt) = @_;
274 $class->_by_phonenum('insert_refund', %opt);
279 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
280 # figure out how to trigger something when those things change
282 # long-term: package changes?
284 =item new_customer OPTION => VALUE, ...
286 Creates a new customer. Takes a list of keys and values as parameters with the
297 first name (required)
305 (not typically collected; mostly used for ACH transactions)
311 =item address1 (required)
315 =item city (required)
323 =item state (required)
345 Currently used for third party tax vendor lookups
349 Used for determining FCC 477 reporting
353 Used for determining FCC 477 reporting
373 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),
375 Set to 1 to enable postal invoicing
379 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
383 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
387 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
391 Expiration date for CARD/DCRD
395 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
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?
431 #same for refnum like signup_server-default_refnum
433 my $cust_main = new FS::cust_main ( {
434 'refnum' => $opt{refnum}
435 || FS::Conf->new->config('signup_server-default_refnum'),
437 'tagnum' => [ FS::part_tag->default_tags ],
439 map { $_ => $opt{$_} } qw(
440 agentnum salesnum refnum agent_custid referral_custnum
442 daytime night fax mobile
443 payby payinfo paydate paycvv payname
448 my @invoicing_list = $opt{'invoicing_list'}
449 ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
451 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
453 my ($bill_hash, $ship_hash);
454 foreach my $f (FS::cust_main->location_fields) {
455 # avoid having to change this in front-end code
456 $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
457 $ship_hash->{$f} = $opt{"ship_$f"};
460 my $bill_location = FS::cust_location->new($bill_hash);
462 # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
463 # so is there a ship address, and if so, is it different from the billing
465 if ( length($ship_hash->{address1}) > 0 and
466 grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
469 $ship_location = FS::cust_location->new( $ship_hash );
472 $ship_location = $bill_location;
475 $cust_main->set('bill_location' => $bill_location);
476 $cust_main->set('ship_location' => $ship_location);
478 $error = $cust_main->insert( {}, \@invoicing_list );
479 return { 'error' => $error } if $error;
481 return { 'error' => '',
482 'custnum' => $cust_main->custnum,
487 =item update_customer
489 Updates an existing customer. Passing an empty value clears that field, while
490 NOT passing that key/value at all leaves it alone. Takes a list of keys and
491 values as parameters with the following keys:
497 API Secret (required)
501 Customer number (required)
557 Comma-separated list of email addresses for email invoices. The special value
558 'POST' is used to designate postal invoicing (it may be specified alone or in
559 addition to email addresses)
563 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
567 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid
568 +"pin" for PREPAY, purchase order number for BILL
572 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
576 Expiration date for CARD/DCRD
580 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
582 =item referral_custnum
584 Referring customer number
598 sub update_customer {
599 my( $class, %opt ) = @_;
600 return _shared_secret_error() unless _check_shared_secret($opt{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 return _shared_secret_error() unless _check_shared_secret($opt{secret});
686 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
687 or return { 'error' => 'Unknown custnum' };
691 'display_custnum' => $cust_main->display_custnum,
692 'name' => $cust_main->first. ' '. $cust_main->get('last'),
693 'balance' => $cust_main->balance,
694 'status' => $cust_main->status,
695 'statuscolor' => $cust_main->statuscolor,
698 $return{$_} = $cust_main->get($_)
699 foreach @cust_main_editable_fields;
701 for (@location_editable_fields) {
702 $return{$_} = $cust_main->bill_location->get($_)
703 if $cust_main->bill_locationnum;
704 $return{'ship_'.$_} = $cust_main->ship_location->get($_)
705 if $cust_main->ship_locationnum;
708 my @invoicing_list = $cust_main->invoicing_list;
709 $return{'invoicing_list'} =
710 join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
711 $return{'postal_invoicing'} =
712 0 < ( grep { $_ eq 'POST' } @invoicing_list );
714 #generally, the more useful data from the cust_main record the better.
715 # well, tell me what you want
724 Returns location specific information for the customer. Takes a list of keys
725 and values as paramters with the following keys: custnum, secret
729 #I also monitor for changes to the additional locations that are applied to
730 # packages, and would like for those to be exportable as well. basically the
731 # location data passed with the custnum.
734 my( $class, %opt ) = @_;
735 return _shared_secret_error() unless _check_shared_secret($opt{secret});
737 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
741 'locations' => [ map $_->hashref, @cust_location ],
747 =item change_package_location
749 Updates package location. Takes a list of keys and values
750 as paramters with the following keys:
756 locationnum - pass this, or the following keys (don't pass both)
788 On error, returns a hashref with an 'error' key.
789 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
790 containing the new values.
794 sub change_package_location {
797 return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
799 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
800 or return { 'error' => 'Unknown pkgnum' };
804 foreach my $field ( qw(
822 $changeopt{$field} = $opt{$field} if $opt{$field};
825 $cust_pkg->API_change(%changeopt);
828 =item bill_now OPTION => VALUE, ...
830 Bills a single customer now, in the same fashion as the "Bill now" link in the
833 Returns a hash reference with a single key, 'error'. If there is an error,
834 the value contains the error, otherwise it is empty. Takes a list of keys and
835 values as parameters with the following keys:
841 API Secret (required)
845 Customer number (required)
852 my( $class, %opt ) = @_;
853 return _shared_secret_error() unless _check_shared_secret($opt{secret});
855 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
856 or return { 'error' => 'Unknown custnum' };
858 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
863 return { 'error' => $error,
869 #next.. Advertising sources?
876 sub _check_shared_secret {
877 shift eq FS::Conf->new->config('api_shared_secret');
880 sub _shared_secret_error {
881 return { 'error' => 'Incorrect shared secret' };