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 return _shared_secret_error() unless _check_shared_secret($opt{secret});
95 #less "raw" than this? we are the backoffice API, and aren't worried
96 # about version migration ala cust_main/cust_location here
97 my $cust_pay = new FS::cust_pay { %opt };
98 my $error = $cust_pay->insert( 'manual'=>1 );
99 return { 'error' => $error,
100 'paynum' => $cust_pay->paynum,
104 # pass the phone number ( from svc_phone )
105 sub insert_payment_phonenum {
106 my($class, %opt) = @_;
107 $class->_by_phonenum('insert_payment', %opt);
111 my($class, $method, %opt) = @_;
112 return _shared_secret_error() unless _check_shared_secret($opt{secret});
114 my $phonenum = delete $opt{'phonenum'};
116 my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
117 or return { 'error' => 'Unknown phonenum' };
119 my $cust_pkg = $svc_phone->cust_svc->cust_pkg
120 or return { 'error' => 'Unlinked phonenum' };
122 $opt{'custnum'} = $cust_pkg->custnum;
124 $class->$method(%opt);
127 =item insert_credit OPTION => VALUE, ...
129 Adds a a credit to a customers account. Takes a list of keys and values as
130 parameters with the following keys
148 The date the credit will be posted
154 my $result = FS::API->insert_credit(
155 'secret' => 'sharingiscaring',
160 '_date' => 1397977200, #UNIX timestamp
163 if ( $result->{'error'} ) {
164 die $result->{'error'};
167 print "crednum ". $result->{'crednum'};
174 my($class, %opt) = @_;
175 return _shared_secret_error() unless _check_shared_secret($opt{secret});
177 $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
179 #less "raw" than this? we are the backoffice API, and aren't worried
180 # about version migration ala cust_main/cust_location here
181 my $cust_credit = new FS::cust_credit { %opt };
182 my $error = $cust_credit->insert;
183 return { 'error' => $error,
184 'crednum' => $cust_credit->crednum,
188 # pass the phone number ( from svc_phone )
189 sub insert_credit_phonenum {
190 my($class, %opt) = @_;
191 $class->_by_phonenum('insert_credit', %opt);
194 =item apply_payments_and_credits
196 Applies payments and credits for this customer. Takes a list of keys and
197 values as parameter with the following keys:
213 #apply payments and credits
214 sub apply_payments_and_credits {
215 my($class, %opt) = @_;
216 return _shared_secret_error() unless _check_shared_secret($opt{secret});
218 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
219 or return { 'error' => 'Unknown custnum' };
221 my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
222 return { 'error' => $error, };
225 =item insert_refund OPTION => VALUE, ...
227 Adds a a credit to a customers account. Takes a list of keys and values as
228 parmeters with the following keys: custnum, payby, refund
232 my $result = FS::API->insert_refund(
233 'secret' => 'sharingiscaring',
239 '_date' => 1397977200, #UNIX timestamp
242 if ( $result->{'error'} ) {
243 die $result->{'error'};
246 print "refundnum ". $result->{'crednum'};
253 my($class, %opt) = @_;
254 return _shared_secret_error() unless _check_shared_secret($opt{secret});
256 # when github pull request #24 is merged,
257 # will have to change over to default reasonnum like credit
258 # but until then, this will do
259 $opt{'reason'} ||= 'API refund';
261 #less "raw" than this? we are the backoffice API, and aren't worried
262 # about version migration ala cust_main/cust_location here
263 my $cust_refund = new FS::cust_refund { %opt };
264 my $error = $cust_refund->insert;
265 return { 'error' => $error,
266 'refundnum' => $cust_refund->refundnum,
270 # pass the phone number ( from svc_phone )
271 sub insert_refund_phonenum {
272 my($class, %opt) = @_;
273 $class->_by_phonenum('insert_refund', %opt);
278 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
279 # figure out how to trigger something when those things change
281 # long-term: package changes?
283 =item new_customer OPTION => VALUE, ...
285 Creates a new customer. Takes a list of keys and values as parameters with the
296 first name (required)
304 (not typically collected; mostly used for ACH transactions)
310 =item address1 (required)
314 =item city (required)
322 =item state (required)
344 Currently used for third party tax vendor lookups
348 Used for determining FCC 477 reporting
352 Used for determining FCC 477 reporting
372 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),
374 Set to 1 to enable postal invoicing
378 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
382 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
386 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
390 Expiration date for CARD/DCRD
394 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
396 =item referral_custnum
398 Referring customer number
410 Agent specific customer number
412 =item referral_custnum
414 Referring customer number
420 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
421 # but approaching this from a clean start / back-office perspective
422 # i.e. no package/service, no immediate credit card run, etc.
425 my( $class, %opt ) = @_;
426 return _shared_secret_error() unless _check_shared_secret($opt{secret});
428 #default agentnum like signup_server-default_agentnum?
430 #same for refnum like signup_server-default_refnum
432 my $cust_main = new FS::cust_main ( {
433 'refnum' => $opt{refnum}
434 || FS::Conf->new->config('signup_server-default_refnum'),
436 'tagnum' => [ FS::part_tag->default_tags ],
438 map { $_ => $opt{$_} } qw(
439 agentnum salesnum refnum agent_custid referral_custnum
441 daytime night fax mobile
442 payby payinfo paydate paycvv payname
447 my @invoicing_list = $opt{'invoicing_list'}
448 ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
450 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
452 my ($bill_hash, $ship_hash);
453 foreach my $f (FS::cust_main->location_fields) {
454 # avoid having to change this in front-end code
455 $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
456 $ship_hash->{$f} = $opt{"ship_$f"};
459 my $bill_location = FS::cust_location->new($bill_hash);
461 # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
462 # so is there a ship address, and if so, is it different from the billing
464 if ( length($ship_hash->{address1}) > 0 and
465 grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
468 $ship_location = FS::cust_location->new( $ship_hash );
471 $ship_location = $bill_location;
474 $cust_main->set('bill_location' => $bill_location);
475 $cust_main->set('ship_location' => $ship_location);
477 $error = $cust_main->insert( {}, \@invoicing_list );
478 return { 'error' => $error } if $error;
480 return { 'error' => '',
481 'custnum' => $cust_main->custnum,
486 =item update_customer
488 Updates an existing customer. Passing an empty value clears that field, while
489 NOT passing that key/value at all leaves it alone. Takes a list of keys and
490 values as parameters with the following keys:
496 API Secret (required)
500 Customer number (required)
556 Comma-separated list of email addresses for email invoices. The special value
557 'POST' is used to designate postal invoicing (it may be specified alone or in
558 addition to email addresses)
562 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
566 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid
567 +"pin" for PREPAY, purchase order number for BILL
571 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
575 Expiration date for CARD/DCRD
579 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
581 =item referral_custnum
583 Referring customer number
597 sub update_customer {
598 my( $class, %opt ) = @_;
599 return _shared_secret_error() unless _check_shared_secret($opt{secret});
601 my $custnum = $opt{'custnum'}
602 or return { 'error' => "no customer record" };
604 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
605 or return { 'error' => "unknown custnum $custnum" };
607 my $new = new FS::cust_main { $cust_main->hash };
609 $new->set( $_ => $opt{$_} )
610 foreach grep { exists $opt{$_} } qw(
611 agentnum salesnum refnum agent_custid referral_custnum
613 daytime night fax mobile
614 payby payinfo paydate paycvv payname
618 if ( exists $opt{'invoicing_list'} || exists $opt{'postal_invoicing'} ) {
619 @invoicing_list = split( /\s*\,\s*/, $opt{'invoicing_list'} );
620 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
622 @invoicing_list = $cust_main->invoicing_list;
625 if ( exists( $opt{'address1'} ) ) {
626 my $bill_location = FS::cust_location->new({
627 map { $_ => $opt{$_} } @location_editable_fields
629 $bill_location->set('custnum' => $custnum);
630 my $error = $bill_location->find_or_insert;
631 die $error if $error;
633 # if this is unchanged from before, cust_main::replace will ignore it
634 $new->set('bill_location' => $bill_location);
637 if ( exists($opt{'ship_address1'}) && length($opt{"ship_address1"}) > 0 ) {
638 my $ship_location = FS::cust_location->new({
639 map { $_ => $opt{"ship_$_"} } @location_editable_fields
642 $ship_location->set('custnum' => $custnum);
643 my $error = $ship_location->find_or_insert;
644 die $error if $error;
646 $new->set('ship_location' => $ship_location);
648 } elsif (exists($opt{'ship_address1'} ) && !grep { length($opt{"ship_$_"}) } @location_editable_fields ) {
649 my $ship_location = $new->bill_location;
650 $new->set('ship_location' => $ship_location);
653 my $error = $new->replace( $cust_main, \@invoicing_list );
654 return { 'error' => $error } if $error;
656 return { 'error' => '',
663 Returns general customer information. Takes a list of keys and values as
664 parameters with the following keys: custnum, secret
668 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
670 use vars qw( @cust_main_editable_fields @location_editable_fields );
671 @cust_main_editable_fields = qw(
672 first last company daytime night fax mobile
675 # payby payinfo payname paystart_month paystart_year payissue payip
676 # ss paytype paystate stateid stateid_state
677 @location_editable_fields = qw(
678 address1 address2 city county state zip country
682 my( $class, %opt ) = @_;
683 return _shared_secret_error() unless _check_shared_secret($opt{secret});
685 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
686 or return { 'error' => 'Unknown custnum' };
690 'display_custnum' => $cust_main->display_custnum,
691 'name' => $cust_main->first. ' '. $cust_main->get('last'),
692 'balance' => $cust_main->balance,
693 'status' => $cust_main->status,
694 'statuscolor' => $cust_main->statuscolor,
697 $return{$_} = $cust_main->get($_)
698 foreach @cust_main_editable_fields;
700 for (@location_editable_fields) {
701 $return{$_} = $cust_main->bill_location->get($_)
702 if $cust_main->bill_locationnum;
703 $return{'ship_'.$_} = $cust_main->ship_location->get($_)
704 if $cust_main->ship_locationnum;
707 my @invoicing_list = $cust_main->invoicing_list;
708 $return{'invoicing_list'} =
709 join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
710 $return{'postal_invoicing'} =
711 0 < ( grep { $_ eq 'POST' } @invoicing_list );
713 #generally, the more useful data from the cust_main record the better.
714 # well, tell me what you want
723 Returns location specific information for the customer. Takes a list of keys
724 and values as paramters with the following keys: custnum, secret
728 #I also monitor for changes to the additional locations that are applied to
729 # packages, and would like for those to be exportable as well. basically the
730 # location data passed with the custnum.
733 my( $class, %opt ) = @_;
734 return _shared_secret_error() unless _check_shared_secret($opt{secret});
736 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
740 'locations' => [ map $_->hashref, @cust_location ],
746 =item bill_now OPTION => VALUE, ...
748 Bills a single customer now, in the same fashion as the "Bill now" link in the
751 Returns a hash reference with a single key, 'error'. If there is an error,
752 the value contains the error, otherwise it is empty. Takes a list of keys and
753 values as parameters with the following keys:
759 API Secret (required)
763 Customer number (required)
770 my( $class, %opt ) = @_;
771 return _shared_secret_error() unless _check_shared_secret($opt{secret});
773 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
774 or return { 'error' => 'Unknown custnum' };
776 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
781 return { 'error' => $error,
787 #next.. Advertising sources?
794 sub _check_shared_secret {
795 shift eq FS::Conf->new->config('api_shared_secret');
798 sub _shared_secret_error {
799 return { 'error' => 'Incorrect shared secret' };