RT# 27969 - created 3 new api functions to add, edit and list advertising sources
[freeside.git] / FS / FS / API.pm
1 package FS::API;
2
3 use FS::Conf;
4 use FS::Record qw( qsearch qsearchs );
5 use FS::cust_main;
6 use FS::cust_location;
7 use FS::cust_pay;
8 use FS::cust_credit;
9 use FS::cust_refund;
10 use FS::cust_pkg;
11
12 =head1 NAME
13
14 FS::API - Freeside backend API
15
16 =head1 SYNOPSIS
17
18   use FS::API;
19
20 =head1 DESCRIPTION
21
22 This module implements a backend API for advanced back-office integration.
23
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 
29 portal functionality.
30
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
35 in plaintext.
36
37 =head1 METHODS
38
39 =over 4
40
41 =item insert_payment OPTION => VALUE, ...
42
43 Adds a new payment to a customers account. Takes a list of keys and values as
44 paramters with the following keys:
45
46 =over 4
47
48 =item secret
49
50 API Secret
51
52 =item custnum
53
54 Customer number
55
56 =item payby
57
58 Payment type
59
60 =item paid
61
62 Amount paid
63
64 =item _date
65
66 Option date for payment
67
68 =item order_number
69
70 Optional order number
71
72 =back
73
74 Example:
75
76   my $result = FS::API->insert_payment(
77     'secret'  => 'sharingiscaring',
78     'custnum' => 181318,
79     'payby'   => 'CASH',
80     'paid'    => '54.32',
81
82     #optional
83     '_date'   => 1397977200, #UNIX timestamp
84     'order_number' => '12345',
85   );
86
87   if ( $result->{'error'} ) {
88     die $result->{'error'};
89   } else {
90     #payment was inserted
91     print "paynum ". $result->{'paynum'};
92   }
93
94 =cut
95
96 #enter cash payment
97 sub insert_payment {
98   my($class, %opt) = @_;
99   return _shared_secret_error() unless _check_shared_secret($opt{secret});
100
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,
107          };
108 }
109
110 # pass the phone number ( from svc_phone ) 
111 sub insert_payment_phonenum {
112   my($class, %opt) = @_;
113   $class->_by_phonenum('insert_payment', %opt);
114 }
115
116 sub _by_phonenum {
117   my($class, $method, %opt) = @_;
118   return _shared_secret_error() unless _check_shared_secret($opt{secret});
119
120   my $phonenum = delete $opt{'phonenum'};
121
122   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
123     or return { 'error' => 'Unknown phonenum' };
124
125   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
126     or return { 'error' => 'Unlinked phonenum' };
127
128   $opt{'custnum'} = $cust_pkg->custnum;
129
130   $class->$method(%opt);
131 }
132
133 =item insert_credit OPTION => VALUE, ...
134
135 Adds a a credit to a customers account.  Takes a list of keys and values as
136 parameters with the following keys
137
138 =over 
139
140 =item secret
141
142 API Secret
143
144 =item custnum
145
146 customer number
147
148 =item amount
149
150 Amount of the credit
151
152 =item _date
153
154 The date the credit will be posted
155
156 =back
157
158 Example:
159
160   my $result = FS::API->insert_credit(
161     'secret'  => 'sharingiscaring',
162     'custnum' => 181318,
163     'amount'  => '54.32',
164
165     #optional
166     '_date'   => 1397977200, #UNIX timestamp
167   );
168
169   if ( $result->{'error'} ) {
170     die $result->{'error'};
171   } else {
172     #credit was inserted
173     print "crednum ". $result->{'crednum'};
174   }
175
176 =cut
177
178 #Enter credit
179 sub insert_credit {
180   my($class, %opt) = @_;
181   return _shared_secret_error() unless _check_shared_secret($opt{secret});
182
183   $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
184
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,
191          };
192 }
193
194 # pass the phone number ( from svc_phone ) 
195 sub insert_credit_phonenum {
196   my($class, %opt) = @_;
197   $class->_by_phonenum('insert_credit', %opt);
198 }
199
200 =item apply_payments_and_credits
201
202 Applies payments and credits for this customer.  Takes a list of keys and
203 values as parameter with the following keys:
204
205 =over 4
206
207 =item secret
208
209 API secret
210
211 =item custnum
212
213 Customer number
214
215 =back
216
217 =cut
218
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});
223
224   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
225     or return { 'error' => 'Unknown custnum' };
226
227   my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
228   return { 'error'  => $error, };
229 }
230
231 =item insert_refund OPTION => VALUE, ...
232
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
235
236 Example:
237
238   my $result = FS::API->insert_refund(
239     'secret'  => 'sharingiscaring',
240     'custnum' => 181318,
241     'payby'   => 'CASH',
242     'refund'  => '54.32',
243
244     #optional
245     '_date'   => 1397977200, #UNIX timestamp
246   );
247
248   if ( $result->{'error'} ) {
249     die $result->{'error'};
250   } else {
251     #refund was inserted
252     print "refundnum ". $result->{'crednum'};
253   }
254
255 =cut
256
257 #Enter cash refund.
258 sub insert_refund {
259   my($class, %opt) = @_;
260   return _shared_secret_error() unless _check_shared_secret($opt{secret});
261
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';
266
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,
273          };
274 }
275
276 # pass the phone number ( from svc_phone ) 
277 sub insert_refund_phonenum {
278   my($class, %opt) = @_;
279   $class->_by_phonenum('insert_refund', %opt);
280 }
281
282 #---
283
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
286
287 # long-term: package changes?
288
289 =item new_customer OPTION => VALUE, ...
290
291 Creates a new customer. Takes a list of keys and values as parameters with the
292 following keys:
293
294 =over 4
295
296 =item secret
297
298 API Secret
299
300 =item first
301
302 first name (required)
303
304 =item last
305
306 last name (required)
307
308 =item ss
309
310 (not typically collected; mostly used for ACH transactions)
311
312 =item company
313
314 Company name
315
316 =item address1 (required)
317
318 Address line one
319
320 =item city (required)
321
322 City
323
324 =item county
325
326 County
327
328 =item state (required)
329
330 State
331
332 =item zip (required)
333
334 Zip or postal code
335
336 =item country
337
338 2 Digit Country Code
339
340 =item latitude
341
342 latitude
343
344 =item Longitude
345
346 longitude
347
348 =item geocode
349
350 Currently used for third party tax vendor lookups
351
352 =item censustract
353
354 Used for determining FCC 477 reporting
355
356 =item censusyear
357
358 Used for determining FCC 477 reporting
359
360 =item daytime
361
362 Daytime phone number
363
364 =item night
365
366 Evening phone number
367
368 =item fax
369
370 Fax number
371
372 =item mobile
373
374 Mobile number
375
376 =item invoicing_list
377
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),
379 postal_invoicing
380 Set to 1 to enable postal invoicing
381
382 =item payby
383
384 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
385
386 =item payinfo
387
388 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
389
390 =item paycvv
391
392 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
393
394 =item paydate
395
396 Expiration date for CARD/DCRD
397
398 =item payname
399
400 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
401
402 =item referral_custnum
403
404 Referring customer number
405
406 =item salesnum
407
408 Sales person number
409
410 =item agentnum
411
412 Agent number
413
414 =item agent_custid
415
416 Agent specific customer number
417
418 =item referral_custnum
419
420 Referring customer number
421
422 =back
423
424 =cut
425
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.
429
430 sub new_customer {
431   my( $class, %opt ) = @_;
432   return _shared_secret_error() unless _check_shared_secret($opt{secret});
433
434   #default agentnum like signup_server-default_agentnum?
435  
436   #same for refnum like signup_server-default_refnum
437
438   my $cust_main = new FS::cust_main ( {
439       'refnum'   => $opt{refnum}
440                     || FS::Conf->new->config('signup_server-default_refnum'),
441       'payby'    => 'BILL',
442       'tagnum'   => [ FS::part_tag->default_tags ],
443
444       map { $_ => $opt{$_} } qw(
445         agentnum salesnum refnum agent_custid referral_custnum
446         last first company 
447         daytime night fax mobile
448         payby payinfo paydate paycvv payname
449       ),
450
451   } );
452
453   my @invoicing_list = $opt{'invoicing_list'}
454                          ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
455                          : ();
456   push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
457
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"};
463   }
464
465   my $bill_location = FS::cust_location->new($bill_hash);
466   my $ship_location;
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 
469   # address?
470   if ( length($ship_hash->{address1}) > 0 and
471           grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
472          ) {
473
474     $ship_location = FS::cust_location->new( $ship_hash );
475   
476   } else {
477     $ship_location = $bill_location;
478   }
479
480   $cust_main->set('bill_location' => $bill_location);
481   $cust_main->set('ship_location' => $ship_location);
482
483   $error = $cust_main->insert( {}, \@invoicing_list );
484   return { 'error'   => $error } if $error;
485   
486   return { 'error'   => '',
487            'custnum' => $cust_main->custnum,
488          };
489
490 }
491
492 =item update_customer
493
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:
497  
498 =over 4
499
500 =item secret
501
502 API Secret (required)
503
504 =item custnum
505
506 Customer number (required)
507
508 =item first
509
510 first name 
511
512 =item last
513
514 last name 
515
516 =item company
517
518 Company name
519
520 =item address1 
521
522 Address line one
523
524 =item city 
525
526 City
527
528 =item county
529
530 County
531
532 =item state 
533
534 State
535
536 =item zip 
537
538 Zip or postal code
539
540 =item country
541
542 2 Digit Country Code
543
544 =item daytime
545
546 Daytime phone number
547
548 =item night
549
550 Evening phone number
551
552 =item fax
553
554 Fax number
555
556 =item mobile
557
558 Mobile number
559
560 =item invoicing_list
561
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)
565
566 =item payby
567
568 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
569
570 =item payinfo
571
572 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid 
573 +"pin" for PREPAY, purchase order number for BILL
574
575 =item paycvv
576
577 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
578
579 =item paydate
580
581 Expiration date for CARD/DCRD
582
583 =item payname
584
585 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
586
587 =item referral_custnum
588
589 Referring customer number
590
591 =item salesnum
592
593 Sales person number
594
595 =item agentnum
596
597 Agent number
598
599 =back
600
601 =cut
602
603 sub update_customer {
604  my( $class, %opt ) = @_;
605   return _shared_secret_error() unless _check_shared_secret($opt{secret});
606
607   my $custnum = $opt{'custnum'}
608     or return { 'error' => "no customer record" };
609
610   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
611     or return { 'error' => "unknown custnum $custnum" };
612
613   my $new = new FS::cust_main { $cust_main->hash };
614
615   $new->set( $_ => $opt{$_} )
616     foreach grep { exists $opt{$_} } qw(
617         agentnum salesnum refnum agent_custid referral_custnum
618         last first company
619         daytime night fax mobile
620         payby payinfo paydate paycvv payname
621       ),
622
623   my @invoicing_list;
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'};
627   } else {
628     @invoicing_list = $cust_main->invoicing_list;
629   }
630  
631   if ( exists( $opt{'address1'} ) ) {
632     my $bill_location = FS::cust_location->new({
633         map { $_ => $opt{$_} } @location_editable_fields
634     });
635     $bill_location->set('custnum' => $custnum);
636     my $error = $bill_location->find_or_insert;
637     die $error if $error;
638
639     # if this is unchanged from before, cust_main::replace will ignore it
640     $new->set('bill_location' => $bill_location);
641   }
642
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
646     });
647
648     $ship_location->set('custnum' => $custnum);
649     my $error = $ship_location->find_or_insert;
650     die $error if $error;
651
652     $new->set('ship_location' => $ship_location);
653
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);
657     }
658
659   my $error = $new->replace( $cust_main, \@invoicing_list );
660   return { 'error'   => $error } if $error;
661
662   return { 'error'   => '',
663          };  
664 }
665
666
667 =item customer_info
668
669 Returns general customer information. Takes a list of keys and values as
670 parameters with the following keys: custnum, secret 
671
672 =cut
673
674 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
675
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
679 );
680 #  locale
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
685 );
686
687 sub customer_info {
688   my( $class, %opt ) = @_;
689   return _shared_secret_error() unless _check_shared_secret($opt{secret});
690
691   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
692     or return { 'error' => 'Unknown custnum' };
693
694   my %return = (
695     'error'           => '',
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,
701   );
702
703   $return{$_} = $cust_main->get($_)
704     foreach @cust_main_editable_fields;
705
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;
711   }
712
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 );
718
719   #generally, the more useful data from the cust_main record the better.
720   # well, tell me what you want
721
722   return \%return;
723
724 }
725
726
727 =item customer_list_svcs OPTION => VALUE, ...
728
729 Returns customer service information.  Takes a list of keys and values as
730 parameters with the following keys: custnum, secret
731
732 =cut
733
734 sub customer_list_svcs {
735   my( $class, %opt ) = @_;
736   return _shared_secret_error() unless _check_shared_secret($opt{secret});
737
738   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
739     or return { 'error' => 'Unknown custnum' };
740
741   #$cust_main->API_list_svcs;
742
743   #false laziness w/ClientAPI/list_svcs
744
745   my @cust_svc = ();
746   #my @cust_pkg_usage = ();
747   #foreach my $cust_pkg ( $p->{'ncancelled'} 
748   #                       ? $cust_main->ncancelled_pkgs
749   #                       : $cust_main->unsuspended_pkgs ) {
750   foreach my $cust_pkg ( $cust_main->all_pkgs ) {
751     #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
752     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
753     #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
754   }
755
756   return {
757     'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
758   };
759
760 }
761
762 =item location_info
763
764 Returns location specific information for the customer. Takes a list of keys
765 and values as paramters with the following keys: custnum, secret
766
767 =cut
768
769 #I also monitor for changes to the additional locations that are applied to
770 # packages, and would like for those to be exportable as well.  basically the
771 # location data passed with the custnum.
772
773 sub location_info {
774   my( $class, %opt ) = @_;
775   return _shared_secret_error() unless _check_shared_secret($opt{secret});
776
777   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
778
779   my %return = (
780     'error'           => '',
781     'locations'       => [ map $_->hashref, @cust_location ],
782   );
783
784   return \%return;
785 }
786
787 =item change_package_location
788
789 Updates package location. Takes a list of keys and values 
790 as paramters with the following keys: 
791
792 pkgnum
793
794 secret
795
796 locationnum - pass this, or the following keys (don't pass both)
797
798 locationname
799
800 address1
801
802 address2
803
804 city
805
806 county
807
808 state
809
810 zip
811
812 addr_clean
813
814 country
815
816 censustract
817
818 censusyear
819
820 location_type
821
822 location_number
823
824 location_kind
825
826 incorporated
827
828 On error, returns a hashref with an 'error' key.
829 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
830 containing the new values.
831
832 =cut
833
834 sub change_package_location {
835   my $class = shift;
836   my %opt  = @_;
837   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
838
839   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
840     or return { 'error' => 'Unknown pkgnum' };
841
842   my %changeopt;
843
844   foreach my $field ( qw(
845     locationnum
846     locationname
847     address1
848     address2
849     city
850     county
851     state
852     zip
853     addr_clean
854     country
855     censustract
856     censusyear
857     location_type
858     location_number
859     location_kind
860     incorporated
861   )) {
862     $changeopt{$field} = $opt{$field} if $opt{$field};
863   }
864
865   $cust_pkg->API_change(%changeopt);
866 }
867
868 =item bill_now OPTION => VALUE, ...
869
870 Bills a single customer now, in the same fashion as the "Bill now" link in the
871 UI.
872
873 Returns a hash reference with a single key, 'error'.  If there is an error,   
874 the value contains the error, otherwise it is empty. Takes a list of keys and
875 values as parameters with the following keys:
876
877 =over 4
878
879 =item secret
880
881 API Secret (required)
882
883 =item custnum
884
885 Customer number (required)
886
887 =back
888
889 =cut
890
891 sub bill_now {
892   my( $class, %opt ) = @_;
893   return _shared_secret_error() unless _check_shared_secret($opt{secret});
894
895   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
896     or return { 'error' => 'Unknown custnum' };
897
898   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
899                                             'retry'      => 1,
900                                             'check_freq' =>'1d',
901                                           );
902
903    return { 'error' => $error,
904           };
905
906 }
907
908
909 #next.. Delete Advertising sources?
910
911 sub list_advertising_sources {
912   my( $class, %opt ) = @_;
913   return _shared_secret_error() unless _check_shared_secret($opt{secret});
914
915   my @sources = qsearch('part_referral', {}, '', "")
916     or return { 'error' => 'No referrals' };
917
918   my $return = {
919     'sources'       => [ map $_->hashref, @sources ],
920   };
921
922   $return;
923 }
924
925 sub add_advertising_source {
926   my( $class, %opt ) = @_;
927   return _shared_secret_error() unless _check_shared_secret($opt{secret});
928
929   use FS::part_referral;
930
931   my $new_source = $opt{source};
932
933   my $source = new FS::part_referral $new_source;
934
935   my $error = $source->insert;
936
937   my $return = {$source->hash};
938   $return = { 'error' => $error, } if $error;
939
940   $return;
941 }
942
943 sub edit_advertising_source {
944   my( $class, %opt ) = @_;
945   return _shared_secret_error() unless _check_shared_secret($opt{secret});
946
947   use FS::part_referral;
948
949   my $refnum = $opt{refnum};
950   my $source = $opt{source};
951
952   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
953   my $new = new FS::part_referral { $old->hash };
954
955   foreach my $key (keys %$source) {
956     $new->$key($source->{$key});
957   }
958
959   my $error = $new->replace;
960
961   my $return = {$new->hash};
962   $return = { 'error' => $error, } if $error;
963
964   $return;
965 }
966
967
968 ##
969 # helper subroutines
970 ##
971
972 sub _check_shared_secret {
973   shift eq FS::Conf->new->config('api_shared_secret');
974 }
975
976 sub _shared_secret_error {
977   return { 'error' => 'Incorrect shared secret' };
978 }
979
980 1;