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.
28 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
29 the port by default, only allow access from back-office servers with the same
30 security precations as the Freeside server, and encrypt the communication
31 channel (for example, with an SSH tunnel or VPN) rather than accessing it
38 =item insert_payment OPTION => VALUE, ...
40 Adds a new payment to a customers account. Takes a list of keys and values as
41 paramters with the following keys:
63 Option date for payment
69 my $result = FS::API->insert_payment(
70 'secret' => 'sharingiscaring',
76 '_date' => 1397977200, #UNIX timestamp
79 if ( $result->{'error'} ) {
80 die $result->{'error'};
83 print "paynum ". $result->{'paynum'};
90 my($class, %opt) = @_;
91 my $conf = new FS::Conf;
92 return { 'error' => 'Incorrect shared secret' }
93 unless $opt{secret} eq $conf->config('api_shared_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 my $conf = new FS::Conf;
108 return { 'error' => 'Incorrect shared secret' }
109 unless $opt{secret} eq $conf->config('api_shared_secret');
111 $class->_by_phonenum('insert_payment', %opt);
116 my($class, $method, %opt) = @_;
117 my $conf = new FS::Conf;
118 return { 'error' => 'Incorrect shared secret' }
119 unless $opt{secret} eq $conf->config('api_shared_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);
135 =item insert_credit OPTION => VALUE, ...
137 Adds a a credit to a customers account. Takes a list of keys and values as
138 parameters with the following keys
156 The date the credit will be posted
162 my $result = FS::API->insert_credit(
163 'secret' => 'sharingiscaring',
168 '_date' => 1397977200, #UNIX timestamp
171 if ( $result->{'error'} ) {
172 die $result->{'error'};
175 print "crednum ". $result->{'crednum'};
182 my($class, %opt) = @_;
183 my $conf = new FS::Conf;
184 return { 'error' => 'Incorrect shared secret' }
185 unless $opt{secret} eq $conf->config('api_shared_secret');
187 $opt{'reasonnum'} ||= $conf->config('api_credit_reason');
189 #less "raw" than this? we are the backoffice API, and aren't worried
190 # about version migration ala cust_main/cust_location here
191 my $cust_credit = new FS::cust_credit { %opt };
192 my $error = $cust_credit->insert;
193 return { 'error' => $error,
194 'crednum' => $cust_credit->crednum,
198 # pass the phone number ( from svc_phone )
199 sub insert_credit_phonenum {
200 my($class, %opt) = @_;
201 my $conf = new FS::Conf;
202 return { 'error' => 'Incorrect shared secret' }
203 unless $opt{secret} eq $conf->config('api_shared_secret');
205 $class->_by_phonenum('insert_credit', %opt);
209 =item insert_refund OPTION => VALUE, ...
211 Adds a a credit to a customers account. Takes a list of keys and values as
212 parmeters with the following keys: custnum, payby, refund
216 my $result = FS::API->insert_refund(
217 'secret' => 'sharingiscaring',
223 '_date' => 1397977200, #UNIX timestamp
226 if ( $result->{'error'} ) {
227 die $result->{'error'};
230 print "refundnum ". $result->{'crednum'};
237 my($class, %opt) = @_;
238 my $conf = new FS::Conf;
239 return { 'error' => 'Incorrect shared secret' }
240 unless $opt{secret} eq $conf->config('api_shared_secret');
242 # when github pull request #24 is merged,
243 # will have to change over to default reasonnum like credit
244 # but until then, this will do
245 $opt{'reason'} ||= 'API refund';
247 #less "raw" than this? we are the backoffice API, and aren't worried
248 # about version migration ala cust_main/cust_location here
249 my $cust_refund = new FS::cust_refund { %opt };
250 my $error = $cust_refund->insert;
251 return { 'error' => $error,
252 'refundnum' => $cust_refund->refundnum,
256 # pass the phone number ( from svc_phone )
257 sub insert_refund_phonenum {
258 my($class, %opt) = @_;
259 my $conf = new FS::Conf;
260 return { 'error' => 'Incorrect shared secret' }
261 unless $opt{secret} eq $conf->config('api_shared_secret');
263 $class->_by_phonenum('insert_refund', %opt);
269 # "2 way syncing" ? start with non-sync pulling info here, then if necessary
270 # figure out how to trigger something when those things change
272 # long-term: package changes?
274 =item new_customer OPTION => VALUE, ...
276 Creates a new customer. Takes a list of keys and values as parameters with the
287 first name (required)
295 (not typically collected; mostly used for ACH transactions)
301 =item address1 (required)
305 =item city (required)
313 =item state (required)
335 Currently used for third party tax vendor lookups
339 Used for determining FCC 477 reporting
343 Used for determining FCC 477 reporting
363 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),
365 Set to 1 to enable postal invoicing
369 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
373 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
377 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
381 Expiration date for CARD/DCRD
385 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
387 =item referral_custnum
389 Referring customer number
401 Agent specific customer number
403 =item referral_custnum
405 Referring customer number
411 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
412 # but approaching this from a clean start / back-office perspective
413 # i.e. no package/service, no immediate credit card run, etc.
416 my( $class, %opt ) = @_;
417 my $conf = new FS::Conf;
418 return { 'error' => 'Incorrect shared secret' }
419 unless $opt{secret} eq $conf->config('api_shared_secret');
421 #default agentnum like signup_server-default_agentnum?
423 #same for refnum like signup_server-default_refnum
425 my $cust_main = new FS::cust_main ( {
426 'agentnum' => $agentnum,
427 'refnum' => $opt{refnum}
428 || $conf->config('signup_server-default_refnum'),
430 'tagnum' => [ FS::part_tag->default_tags ],
432 map { $_ => $opt{$_} } qw(
433 agentnum salesnum refnum agent_custid referral_custnum
435 daytime night fax mobile
436 payby payinfo paydate paycvv payname
441 my @invoicing_list = $opt{'invoicing_list'}
442 ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
444 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
446 my ($bill_hash, $ship_hash);
447 foreach my $f (FS::cust_main->location_fields) {
448 # avoid having to change this in front-end code
449 $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
450 $ship_hash->{$f} = $opt{"ship_$f"};
453 my $bill_location = FS::cust_location->new($bill_hash);
455 # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
456 # so is there a ship address, and if so, is it different from the billing
458 if ( length($ship_hash->{address1}) > 0 and
459 grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
462 $ship_location = FS::cust_location->new( $ship_hash );
465 $ship_location = $bill_location;
468 $cust_main->set('bill_location' => $bill_location);
469 $cust_main->set('ship_location' => $ship_location);
471 $error = $cust_main->insert( {}, \@invoicing_list );
472 return { 'error' => $error } if $error;
474 return { 'error' => '',
475 'custnum' => $cust_main->custnum,
480 =item update_customer
482 Updates an existing customer. Passing an empty value clears that field, while
483 NOT passing that key/value at all leaves it alone. Takes a list of keys and
484 values as parameters with the following keys:
490 API Secret (required)
494 Customer number (required)
550 Comma-separated list of email addresses for email invoices. The special value
551 'POST' is used to designate postal invoicing (it may be specified alone or in
552 addition to email addresses)
556 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
560 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid
561 +"pin" for PREPAY, purchase order number for BILL
565 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
569 Expiration date for CARD/DCRD
573 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
575 =item referral_custnum
577 Referring customer number
591 sub update_customer {
593 my( $class, %opt ) = @_;
595 my $conf = new FS::Conf;
596 return { 'error' => 'Incorrect shared secret' }
597 unless $opt{secret} eq $conf->config('api_shared_secret');
600 my $custnum = $opt{'custnum'}
601 or return { 'error' => "no customer record" };
603 my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
604 or return { 'error' => "unknown custnum $custnum" };
606 my $new = new FS::cust_main { $cust_main->hash };
608 $new->set( $_ => $opt{$_} )
609 foreach grep { exists $opt{$_} } qw(
610 agentnum salesnum refnum agent_custid referral_custnum
612 daytime night fax mobile
613 payby payinfo paydate paycvv payname
617 if ( exists $opt{'invoicing_list'} || exists $opt{'postal_invoicing'} ) {
618 @invoicing_list = split( /\s*\,\s*/, $opt{'invoicing_list'} );
619 push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
621 @invoicing_list = $cust_main->invoicing_list;
624 if ( exists( $opt{'address1'} ) ) {
625 my $bill_location = FS::cust_location->new({
626 map { $_ => $opt{$_} } @location_editable_fields
628 $bill_location->set('custnum' => $custnum);
629 my $error = $bill_location->find_or_insert;
630 die $error if $error;
632 # if this is unchanged from before, cust_main::replace will ignore it
633 $new->set('bill_location' => $bill_location);
636 if ( exists($opt{'ship_address1'}) && length($opt{"ship_address1"}) > 0 ) {
637 my $ship_location = FS::cust_location->new({
638 map { $_ => $opt{"ship_$_"} } @location_editable_fields
641 $ship_location->set('custnum' => $custnum);
642 my $error = $ship_location->find_or_insert;
643 die $error if $error;
645 $new->set('ship_location' => $ship_location);
647 } elsif (exists($opt{'ship_address1'} ) && !grep { length($opt{"ship_$_"}) } @location_editable_fields ) {
648 my $ship_location = $new->bill_location;
649 $new->set('ship_location' => $ship_location);
652 my $error = $new->replace( $cust_main, \@invoicing_list );
653 return { 'error' => $error } if $error;
655 return { 'error' => '',
662 Returns general customer information. Takes a list of keys and values as
663 parameters with the following keys: custnum, secret
667 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
669 use vars qw( @cust_main_editable_fields @location_editable_fields );
670 @cust_main_editable_fields = qw(
671 first last company daytime night fax mobile
674 # payby payinfo payname paystart_month paystart_year payissue payip
675 # ss paytype paystate stateid stateid_state
676 @location_editable_fields = qw(
677 address1 address2 city county state zip country
681 my( $class, %opt ) = @_;
682 my $conf = new FS::Conf;
683 return { 'error' => 'Incorrect shared secret' }
684 unless $opt{secret} eq $conf->config('api_shared_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 my $conf = new FS::Conf;
736 return { 'error' => 'Incorrect shared secret' }
737 unless $opt{secret} eq $conf->config('api_shared_secret');
739 my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
743 'locations' => [ map $_->hashref, @cust_location ],
749 =item bill_now OPTION => VALUE, ...
751 Bills a single customer now, in the same fashion as the "Bill now" link in the
754 Returns a hash reference with a single key, 'error'. If there is an error,
755 the value contains the error, otherwise it is empty. Takes a list of keys and
756 values as parameters with the following keys:
762 API Secret (required)
766 Customer number (required)
773 my( $class, %opt ) = @_;
774 my $conf = new FS::Conf;
775 return { 'error' => 'Incorrect shared secret' }
776 unless $opt{secret} eq $conf->config('api_shared_secret');
778 my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
779 or return { 'error' => 'Unknown custnum' };
781 my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
786 return { 'error' => $error,
792 #Advertising sources?