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
76 my $result = FS::API->insert_payment(
77 'secret' => 'sharingiscaring',
83 '_date' => 1397977200, #UNIX timestamp
84 'order_number' => '12345',
87 if ( $result->{'error'} ) {
88 die $result->{'error'};
91 print "paynum ". $result->{'paynum'};
98 my($class, %opt) = @_;
99 return _shared_secret_error() unless _check_shared_secret($opt{secret});
101 #less "raw" than this? we are the backoffice API, and aren't worried
102 # about version migration ala cust_main/cust_location here
103 my $cust_pay = new FS::cust_pay { %opt };
104 my $error = $cust_pay->insert( 'manual'=>1 );
105 return { 'error' => $error,
106 'paynum' => $cust_pay->paynum,
110 # pass the phone number ( from svc_phone )
111 sub insert_payment_phonenum {
112 my($class, %opt) = @_;
113 $class->_by_phonenum('insert_payment', %opt);
117 my($class, $method, %opt) = @_;
118 return _shared_secret_error() unless _check_shared_secret($opt{secret});
120 my $phonenum = delete $opt{'phonenum'};
122 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
123 or return { 'error' => 'Unknown phonenum' };
125 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
126 or return { 'error' => 'Unlinked phonenum' };
128 $opt{'custnum'} = $cust_pkg->custnum;
130 $class->$method(%opt);
133 =item insert_credit OPTION => VALUE, ...
135 Adds a a credit to a customers account. Takes a list of keys and values as
136 parameters with the following keys
154 The date the credit will be posted
160 my $result = FS::API->insert_credit(
161 'secret' => 'sharingiscaring',
166 '_date' => 1397977200, #UNIX timestamp
169 if ( $result->{'error'} ) {
170 die $result->{'error'};
173 print "crednum ". $result->{'crednum'};
180 my($class, %opt) = @_;
181 return _shared_secret_error() unless _check_shared_secret($opt{secret});
183 $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
185 #less "raw" than this? we are the backoffice API, and aren't worried
186 # about version migration ala cust_main/cust_location here
187 my $cust_credit = new FS::cust_credit { %opt };
188 my $error = $cust_credit->insert;
189 return { 'error' => $error,
190 'crednum' => $cust_credit->crednum,
194 # pass the phone number ( from svc_phone )
195 sub insert_credit_phonenum {
196 my($class, %opt) = @_;
197 $class->_by_phonenum('insert_credit', %opt);
200 =item apply_payments_and_credits
202 Applies payments and credits for this customer. Takes a list of keys and
203 values as parameter with the following keys:
219 #apply payments and credits
220 sub apply_payments_and_credits {
221 my($class, %opt) = @_;
222 return _shared_secret_error() unless _check_shared_secret($opt{secret});
224 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
225 or return { 'error' => 'Unknown custnum' };
227 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
228 return { 'error' => $error, };
231 =item insert_refund OPTION => VALUE, ...
233 Adds a a credit to a customers account. Takes a list of keys and values as
234 parmeters with the following keys: custnum, payby, refund
238 my $result = FS::API->insert_refund(
239 'secret' => 'sharingiscaring',
245 '_date' => 1397977200, #UNIX timestamp
248 if ( $result->{'error'} ) {
249 die $result->{'error'};
252 print "refundnum ". $result->{'crednum'};
259 my($class, %opt) = @_;
260 return _shared_secret_error() unless _check_shared_secret($opt{secret});
262 # when github pull request #24 is merged,
263 # will have to change over to default reasonnum like credit
264 # but until then, this will do
265 $opt{'reason'} ||= 'API refund';
267 #less "raw" than this? we are the backoffice API, and aren't worried
268 # about version migration ala cust_main/cust_location here
269 my $cust_refund = new FS::cust_refund { %opt };
270 my $error = $cust_refund->insert;
271 return { 'error' => $error,
272 'refundnum' => $cust_refund->refundnum,
276 # pass the phone number ( from svc_phone )
277 sub insert_refund_phonenum {
278 my($class, %opt) = @_;
279 $class->_by_phonenum('insert_refund', %opt);
284 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
285 # figure out how to trigger something when those things change
287 # long-term: package changes?
289 =item new_customer OPTION => VALUE, ...
291 Creates a new customer. Takes a list of keys and values as parameters with the
302 first name (required)
310 (not typically collected; mostly used for ACH transactions)
316 =item address1 (required)
320 =item city (required)
328 =item state (required)
350 Currently used for third party tax vendor lookups
354 Used for determining FCC 477 reporting
358 Used for determining FCC 477 reporting
378 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),
380 Set to 1 to enable postal invoicing
384 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
388 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
392 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
396 Expiration date for CARD/DCRD
400 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
402 =item referral_custnum
404 Referring customer number
416 Agent specific customer number
418 =item referral_custnum
420 Referring customer number
426 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
427 # but approaching this from a clean start / back-office perspective
428 # i.e. no package/service, no immediate credit card run, etc.
431 my( $class, %opt ) = @_;
432 return _shared_secret_error() unless _check_shared_secret($opt{secret});
434 #default agentnum like signup_server-default_agentnum?
436 #same for refnum like signup_server-default_refnum
438 my $cust_main = new FS::cust_main ( {
439 'refnum' => $opt{refnum}
440 || FS::Conf->new->config('signup_server-default_refnum'),
442 'tagnum' => [ FS::part_tag->default_tags ],
444 map { $_ => $opt{$_} } qw(
445 agentnum salesnum refnum agent_custid referral_custnum
447 daytime night fax mobile
448 payby payinfo paydate paycvv payname
453 my @invoicing_list = $opt{'invoicing_list'}
454 ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
456 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
458 my ($bill_hash, $ship_hash);
459 foreach my $f (FS::cust_main->location_fields) {
460 # avoid having to change this in front-end code
461 $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
462 $ship_hash->{$f} = $opt{"ship_$f"};
465 my $bill_location = FS::cust_location->new($bill_hash);
467 # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
468 # so is there a ship address, and if so, is it different from the billing
470 if ( length($ship_hash->{address1}) > 0 and
471 grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
474 $ship_location = FS::cust_location->new( $ship_hash );
477 $ship_location = $bill_location;
480 $cust_main->set('bill_location' => $bill_location);
481 $cust_main->set('ship_location' => $ship_location);
483 $error = $cust_main->insert( {}, \@invoicing_list );
484 return { 'error' => $error } if $error;
486 return { 'error' => '',
487 'custnum' => $cust_main->custnum,
492 =item update_customer
494 Updates an existing customer. Passing an empty value clears that field, while
495 NOT passing that key/value at all leaves it alone. Takes a list of keys and
496 values as parameters with the following keys:
502 API Secret (required)
506 Customer number (required)
562 Comma-separated list of email addresses for email invoices. The special value
563 'POST' is used to designate postal invoicing (it may be specified alone or in
564 addition to email addresses)
568 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
572 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid
573 +"pin" for PREPAY, purchase order number for BILL
577 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
581 Expiration date for CARD/DCRD
585 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
587 =item referral_custnum
589 Referring customer number
603 sub update_customer {
604 my( $class, %opt ) = @_;
605 return _shared_secret_error() unless _check_shared_secret($opt{secret});
607 my $custnum = $opt{'custnum'}
608 or return { 'error' => "no customer record" };
610 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
611 or return { 'error' => "unknown custnum $custnum" };
613 my $new = new FS::cust_main { $cust_main->hash };
615 $new->set( $_ => $opt{$_} )
616 foreach grep { exists $opt{$_} } qw(
617 agentnum salesnum refnum agent_custid referral_custnum
619 daytime night fax mobile
620 payby payinfo paydate paycvv payname
624 if ( exists $opt{'invoicing_list'} || exists $opt{'postal_invoicing'} ) {
625 @invoicing_list = split( /\s*\,\s*/, $opt{'invoicing_list'} );
626 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
628 @invoicing_list = $cust_main->invoicing_list;
631 if ( exists( $opt{'address1'} ) ) {
632 my $bill_location = FS::cust_location->new({
633 map { $_ => $opt{$_} } @location_editable_fields
635 $bill_location->set('custnum' => $custnum);
636 my $error = $bill_location->find_or_insert;
637 die $error if $error;
639 # if this is unchanged from before, cust_main::replace will ignore it
640 $new->set('bill_location' => $bill_location);
643 if ( exists($opt{'ship_address1'}) && length($opt{"ship_address1"}) > 0 ) {
644 my $ship_location = FS::cust_location->new({
645 map { $_ => $opt{"ship_$_"} } @location_editable_fields
648 $ship_location->set('custnum' => $custnum);
649 my $error = $ship_location->find_or_insert;
650 die $error if $error;
652 $new->set('ship_location' => $ship_location);
654 } elsif (exists($opt{'ship_address1'} ) && !grep { length($opt{"ship_$_"}) } @location_editable_fields ) {
655 my $ship_location = $new->bill_location;
656 $new->set('ship_location' => $ship_location);
659 my $error = $new->replace( $cust_main, \@invoicing_list );
660 return { 'error' => $error } if $error;
662 return { 'error' => '',
669 Returns general customer information. Takes a list of keys and values as
670 parameters with the following keys: custnum, secret
674 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
676 use vars qw( @cust_main_editable_fields @location_editable_fields );
677 @cust_main_editable_fields = qw(
678 first last company daytime night fax mobile
681 # payby payinfo payname paystart_month paystart_year payissue payip
682 # ss paytype paystate stateid stateid_state
683 @location_editable_fields = qw(
684 address1 address2 city county state zip country
688 my( $class, %opt ) = @_;
689 return _shared_secret_error() unless _check_shared_secret($opt{secret});
691 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
692 or return { 'error' => 'Unknown custnum' };
696 'display_custnum' => $cust_main->display_custnum,
697 'name' => $cust_main->first. ' '. $cust_main->get('last'),
698 'balance' => $cust_main->balance,
699 'status' => $cust_main->status,
700 'statuscolor' => $cust_main->statuscolor,
703 $return{$_} = $cust_main->get($_)
704 foreach @cust_main_editable_fields;
706 for (@location_editable_fields) {
707 $return{$_} = $cust_main->bill_location->get($_)
708 if $cust_main->bill_locationnum;
709 $return{'ship_'.$_} = $cust_main->ship_location->get($_)
710 if $cust_main->ship_locationnum;
713 my @invoicing_list = $cust_main->invoicing_list;
714 $return{'invoicing_list'} =
715 join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
716 $return{'postal_invoicing'} =
717 0 < ( grep { $_ eq 'POST' } @invoicing_list );
719 #generally, the more useful data from the cust_main record the better.
720 # well, tell me what you want
729 Returns location specific information for the customer. Takes a list of keys
730 and values as paramters with the following keys: custnum, secret
734 #I also monitor for changes to the additional locations that are applied to
735 # packages, and would like for those to be exportable as well. basically the
736 # location data passed with the custnum.
739 my( $class, %opt ) = @_;
740 return _shared_secret_error() unless _check_shared_secret($opt{secret});
742 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
746 'locations' => [ map $_->hashref, @cust_location ],
752 =item change_package_location
754 Updates package location. Takes a list of keys and values
755 as paramters with the following keys:
761 locationnum - pass this, or the following keys (don't pass both)
793 On error, returns a hashref with an 'error' key.
794 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
795 containing the new values.
799 sub change_package_location {
802 return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
804 my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
805 or return { 'error' => 'Unknown pkgnum' };
809 foreach my $field ( qw(
827 $changeopt{$field} = $opt{$field} if $opt{$field};
830 $cust_pkg->API_change(%changeopt);
833 =item bill_now OPTION => VALUE, ...
835 Bills a single customer now, in the same fashion as the "Bill now" link in the
838 Returns a hash reference with a single key, 'error'. If there is an error,
839 the value contains the error, otherwise it is empty. Takes a list of keys and
840 values as parameters with the following keys:
846 API Secret (required)
850 Customer number (required)
857 my( $class, %opt ) = @_;
858 return _shared_secret_error() unless _check_shared_secret($opt{secret});
860 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
861 or return { 'error' => 'Unknown custnum' };
863 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
868 return { 'error' => $error,
874 #next.. Advertising sources?
881 sub _check_shared_secret {
882 shift eq FS::Conf->new->config('api_shared_secret');
885 sub _shared_secret_error {
886 return { 'error' => 'Incorrect shared secret' };