6e09713b23f15cd5c6b1411c3e597baa059104d6
[freeside.git] / FS / FS / API.pm
1 package FS::API;
2
3 use strict;
4 use FS::Conf;
5 use FS::Record qw( qsearch qsearchs );
6 use FS::cust_main;
7 use FS::cust_location;
8 use FS::cust_pay;
9 use FS::cust_credit;
10 use FS::cust_refund;
11 use FS::cust_pkg;
12
13 =head1 NAME
14
15 FS::API - Freeside backend API
16
17 =head1 SYNOPSIS
18
19   use Frontier::Client;
20   use Data::Dumper;
21
22   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
23                                               # the traffic
24
25   my $xmlrpc = new Frontier::Client url=>$url;
26
27   my $result = $xmlrpc->call( 'FS.API.customer_info',
28                                 'secret'  => 'sharingiscaring',
29                                 'custnum' => 181318,
30                             );
31
32   print Dumper($result);
33
34 =head1 DESCRIPTION
35
36 This module implements a backend API for advanced back-office integration.
37
38 In contrast to the self-service API, which authenticates an end-user and offers
39 functionality to that end user, the backend API performs a simple shared-secret
40 authentication and offers full, administrator functionality, enabling
41 integration with other back-office systems.  Only access this API from a secure 
42 network from other backoffice machines. DON'T use this API to create customer 
43 portal functionality.
44
45 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
46 the port by default, only allow access from back-office servers with the same
47 security precations as the Freeside server, and encrypt the communication
48 channel (for example, with an SSH tunnel or VPN) rather than accessing it
49 in plaintext.
50
51 =head1 METHODS
52
53 =over 4
54
55 =item insert_payment OPTION => VALUE, ...
56
57 Adds a new payment to a customers account. Takes a list of keys and values as
58 paramters with the following keys:
59
60 =over 4
61
62 =item secret
63
64 API Secret
65
66 =item custnum
67
68 Customer number
69
70 =item payby
71
72 Payment type
73
74 =item paid
75
76 Amount paid
77
78 =item _date
79
80 Option date for payment
81
82 =item order_number
83
84 Optional order number
85
86 =back
87
88 Example:
89
90   my $result = FS::API->insert_payment(
91     'secret'  => 'sharingiscaring',
92     'custnum' => 181318,
93     'payby'   => 'CASH',
94     'paid'    => '54.32',
95
96     #optional
97     '_date'   => 1397977200, #UNIX timestamp
98     'order_number' => '12345',
99   );
100
101   if ( $result->{'error'} ) {
102     die $result->{'error'};
103   } else {
104     #payment was inserted
105     print "paynum ". $result->{'paynum'};
106   }
107
108 =cut
109
110 #enter cash payment
111 sub insert_payment {
112   my($class, %opt) = @_;
113   return _shared_secret_error() unless _check_shared_secret($opt{secret});
114
115   #less "raw" than this?  we are the backoffice API, and aren't worried
116   # about version migration ala cust_main/cust_location here
117   my $cust_pay = new FS::cust_pay { %opt };
118   my $error = $cust_pay->insert( 'manual'=>1 );
119   return { 'error'  => $error,
120            'paynum' => $cust_pay->paynum,
121          };
122 }
123
124 # pass the phone number ( from svc_phone ) 
125 sub insert_payment_phonenum {
126   my($class, %opt) = @_;
127   $class->_by_phonenum('insert_payment', %opt);
128 }
129
130 sub _by_phonenum {
131   my($class, $method, %opt) = @_;
132   return _shared_secret_error() unless _check_shared_secret($opt{secret});
133
134   my $phonenum = delete $opt{'phonenum'};
135
136   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
137     or return { 'error' => 'Unknown phonenum' };
138
139   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
140     or return { 'error' => 'Unlinked phonenum' };
141
142   $opt{'custnum'} = $cust_pkg->custnum;
143
144   $class->$method(%opt);
145 }
146
147 =item insert_credit OPTION => VALUE, ...
148
149 Adds a a credit to a customers account.  Takes a list of keys and values as
150 parameters with the following keys
151
152 =over 
153
154 =item secret
155
156 API Secret
157
158 =item custnum
159
160 customer number
161
162 =item amount
163
164 Amount of the credit
165
166 =item _date
167
168 The date the credit will be posted
169
170 =back
171
172 Example:
173
174   my $result = FS::API->insert_credit(
175     'secret'  => 'sharingiscaring',
176     'custnum' => 181318,
177     'amount'  => '54.32',
178
179     #optional
180     '_date'   => 1397977200, #UNIX timestamp
181   );
182
183   if ( $result->{'error'} ) {
184     die $result->{'error'};
185   } else {
186     #credit was inserted
187     print "crednum ". $result->{'crednum'};
188   }
189
190 =cut
191
192 #Enter credit
193 sub insert_credit {
194   my($class, %opt) = @_;
195   return _shared_secret_error() unless _check_shared_secret($opt{secret});
196
197   $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
198
199   #less "raw" than this?  we are the backoffice API, and aren't worried
200   # about version migration ala cust_main/cust_location here
201   my $cust_credit = new FS::cust_credit { %opt };
202   my $error = $cust_credit->insert;
203   return { 'error'  => $error,
204            'crednum' => $cust_credit->crednum,
205          };
206 }
207
208 # pass the phone number ( from svc_phone ) 
209 sub insert_credit_phonenum {
210   my($class, %opt) = @_;
211   $class->_by_phonenum('insert_credit', %opt);
212 }
213
214 =item apply_payments_and_credits
215
216 Applies payments and credits for this customer.  Takes a list of keys and
217 values as parameter with the following keys:
218
219 =over 4
220
221 =item secret
222
223 API secret
224
225 =item custnum
226
227 Customer number
228
229 =back
230
231 =cut
232
233 #apply payments and credits
234 sub apply_payments_and_credits {
235   my($class, %opt) = @_;
236   return _shared_secret_error() unless _check_shared_secret($opt{secret});
237
238   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
239     or return { 'error' => 'Unknown custnum' };
240
241   my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
242   return { 'error'  => $error, };
243 }
244
245 =item insert_refund OPTION => VALUE, ...
246
247 Adds a a credit to a customers account.  Takes a list of keys and values as
248 parmeters with the following keys: custnum, payby, refund
249
250 Example:
251
252   my $result = FS::API->insert_refund(
253     'secret'  => 'sharingiscaring',
254     'custnum' => 181318,
255     'payby'   => 'CASH',
256     'refund'  => '54.32',
257
258     #optional
259     '_date'   => 1397977200, #UNIX timestamp
260   );
261
262   if ( $result->{'error'} ) {
263     die $result->{'error'};
264   } else {
265     #refund was inserted
266     print "refundnum ". $result->{'crednum'};
267   }
268
269 =cut
270
271 #Enter cash refund.
272 sub insert_refund {
273   my($class, %opt) = @_;
274   return _shared_secret_error() unless _check_shared_secret($opt{secret});
275
276   # when github pull request #24 is merged,
277   #  will have to change over to default reasonnum like credit
278   # but until then, this will do
279   $opt{'reason'} ||= 'API refund';
280
281   #less "raw" than this?  we are the backoffice API, and aren't worried
282   # about version migration ala cust_main/cust_location here
283   my $cust_refund = new FS::cust_refund { %opt };
284   my $error = $cust_refund->insert;
285   return { 'error'     => $error,
286            'refundnum' => $cust_refund->refundnum,
287          };
288 }
289
290 # pass the phone number ( from svc_phone ) 
291 sub insert_refund_phonenum {
292   my($class, %opt) = @_;
293   $class->_by_phonenum('insert_refund', %opt);
294 }
295
296 #---
297
298 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
299 # figure out how to trigger something when those things change
300
301 # long-term: package changes?
302
303 =item new_customer OPTION => VALUE, ...
304
305 Creates a new customer. Takes a list of keys and values as parameters with the
306 following keys:
307
308 =over 4
309
310 =item secret
311
312 API Secret
313
314 =item first
315
316 first name (required)
317
318 =item last
319
320 last name (required)
321
322 =item ss
323
324 (not typically collected; mostly used for ACH transactions)
325
326 =item company
327
328 Company name
329
330 =item address1 (required)
331
332 Address line one
333
334 =item city (required)
335
336 City
337
338 =item county
339
340 County
341
342 =item state (required)
343
344 State
345
346 =item zip (required)
347
348 Zip or postal code
349
350 =item country
351
352 2 Digit Country Code
353
354 =item latitude
355
356 latitude
357
358 =item Longitude
359
360 longitude
361
362 =item geocode
363
364 Currently used for third party tax vendor lookups
365
366 =item censustract
367
368 Used for determining FCC 477 reporting
369
370 =item censusyear
371
372 Used for determining FCC 477 reporting
373
374 =item daytime
375
376 Daytime phone number
377
378 =item night
379
380 Evening phone number
381
382 =item fax
383
384 Fax number
385
386 =item mobile
387
388 Mobile number
389
390 =item invoicing_list
391
392 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),
393 postal_invoicing
394 Set to 1 to enable postal invoicing
395
396 =item referral_custnum
397
398 Referring customer number
399
400 =item salesnum
401
402 Sales person number
403
404 =item agentnum
405
406 Agent number
407
408 =item agent_custid
409
410 Agent specific customer number
411
412 =item referral_custnum
413
414 Referring customer number
415
416 =back
417
418 =cut
419
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.
423
424 sub new_customer {
425   my( $class, %opt ) = @_;
426   return _shared_secret_error() unless _check_shared_secret($opt{secret});
427
428   #default agentnum like signup_server-default_agentnum?
429   #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
430  
431   #same for refnum like signup_server-default_refnum
432   $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
433
434   $class->API_insert( %opt );
435 }
436
437 =item update_customer
438
439 Updates an existing customer. Passing an empty value clears that field, while
440 NOT passing that key/value at all leaves it alone. Takes a list of keys and
441 values as parameters with the following keys:
442
443 =over 4
444
445 =item secret
446
447 API Secret (required)
448
449 =item custnum
450
451 Customer number (required)
452
453 =item first
454
455 first name 
456
457 =item last
458
459 last name 
460
461 =item company
462
463 Company name
464
465 =item address1 
466
467 Address line one
468
469 =item city 
470
471 City
472
473 =item county
474
475 County
476
477 =item state 
478
479 State
480
481 =item zip 
482
483 Zip or postal code
484
485 =item country
486
487 2 Digit Country Code
488
489 =item daytime
490
491 Daytime phone number
492
493 =item night
494
495 Evening phone number
496
497 =item fax
498
499 Fax number
500
501 =item mobile
502
503 Mobile number
504
505 =item invoicing_list
506
507 Comma-separated list of email addresses for email invoices. The special value 
508 'POST' is used to designate postal invoicing (it may be specified alone or in
509 addition to email addresses),
510 postal_invoicing
511 Set to 1 to enable postal invoicing
512
513 =item referral_custnum
514
515 Referring customer number
516
517 =item salesnum
518
519 Sales person number
520
521 =item agentnum
522
523 Agent number
524
525 =back
526
527 =cut
528
529 sub update_customer {
530   my( $class, %opt ) = @_;
531   return _shared_secret_error() unless _check_shared_secret($opt{secret});
532
533   FS::cust_main->API_update( %opt );
534 }
535
536 =item customer_info OPTION => VALUE, ...
537
538 Returns general customer information. Takes a list of keys and values as
539 parameters with the following keys: custnum, secret 
540
541 Example:
542
543   use Frontier::Client;
544   use Data::Dumper;
545
546   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
547                                               # the traffic
548
549   my $xmlrpc = new Frontier::Client url=>$url;
550
551   my $result = $xmlrpc->call( 'FS.API.customer_info',
552                                 'secret'  => 'sharingiscaring',
553                                 'custnum' => 181318,
554                             );
555
556   print Dumper($result);
557
558 =cut
559
560 sub customer_info {
561   my( $class, %opt ) = @_;
562   return _shared_secret_error() unless _check_shared_secret($opt{secret});
563
564   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
565     or return { 'error' => 'Unknown custnum' };
566
567   $cust_main->API_getinfo;
568 }
569
570 =item customer_list_svcs OPTION => VALUE, ...
571
572 Returns customer service information.  Takes a list of keys and values as
573 parameters with the following keys: custnum, secret
574
575 Example:
576
577   use Frontier::Client;
578   use Data::Dumper;
579
580   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
581                                               # the traffic
582
583   my $xmlrpc = new Frontier::Client url=>$url;
584
585   my $result = $xmlrpc->call( 'FS.API.customer_list_svcs',
586                                 'secret'  => 'sharingiscaring',
587                                 'custnum' => 181318,
588                             );
589
590   print Dumper($result);
591
592   foreach my $cust_svc ( @{ $result->{'cust_svc'} } ) {
593     #print $cust_svc->{mac_addr}."\n" if exists $cust_svc->{mac_addr};
594     print $cust_svc->{circuit_id}."\n" if exists $cust_svc->{circuit_id};
595   }
596
597 =cut
598
599 sub customer_list_svcs {
600   my( $class, %opt ) = @_;
601   return _shared_secret_error() unless _check_shared_secret($opt{secret});
602
603   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
604     or return { 'error' => 'Unknown custnum' };
605
606   #$cust_main->API_list_svcs;
607
608   #false laziness w/ClientAPI/list_svcs
609
610   my @cust_svc = ();
611   #my @cust_pkg_usage = ();
612   #foreach my $cust_pkg ( $p->{'ncancelled'} 
613   #                       ? $cust_main->ncancelled_pkgs
614   #                       : $cust_main->unsuspended_pkgs ) {
615   foreach my $cust_pkg ( $cust_main->all_pkgs ) {
616     #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
617     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
618     #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
619   }
620
621   return {
622     'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
623   };
624
625 }
626
627 =item location_info
628
629 Returns location specific information for the customer. Takes a list of keys
630 and values as paramters with the following keys: custnum, secret
631
632 =cut
633
634 #I also monitor for changes to the additional locations that are applied to
635 # packages, and would like for those to be exportable as well.  basically the
636 # location data passed with the custnum.
637
638 sub location_info {
639   my( $class, %opt ) = @_;
640   return _shared_secret_error() unless _check_shared_secret($opt{secret});
641
642   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
643
644   my %return = (
645     'error'           => '',
646     'locations'       => [ map $_->hashref, @cust_location ],
647   );
648
649   return \%return;
650 }
651
652 =item change_package_location
653
654 Updates package location. Takes a list of keys and values 
655 as paramters with the following keys: 
656
657 pkgnum
658
659 secret
660
661 locationnum - pass this, or the following keys (don't pass both)
662
663 locationname
664
665 address1
666
667 address2
668
669 city
670
671 county
672
673 state
674
675 zip
676
677 addr_clean
678
679 country
680
681 censustract
682
683 censusyear
684
685 location_type
686
687 location_number
688
689 location_kind
690
691 incorporated
692
693 On error, returns a hashref with an 'error' key.
694 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
695 containing the new values.
696
697 =cut
698
699 sub change_package_location {
700   my $class = shift;
701   my %opt  = @_;
702   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
703
704   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
705     or return { 'error' => 'Unknown pkgnum' };
706
707   my %changeopt;
708
709   foreach my $field ( qw(
710     locationnum
711     locationname
712     address1
713     address2
714     city
715     county
716     state
717     zip
718     addr_clean
719     country
720     censustract
721     censusyear
722     location_type
723     location_number
724     location_kind
725     incorporated
726   )) {
727     $changeopt{$field} = $opt{$field} if $opt{$field};
728   }
729
730   $cust_pkg->API_change(%changeopt);
731 }
732
733 =item bill_now OPTION => VALUE, ...
734
735 Bills a single customer now, in the same fashion as the "Bill now" link in the
736 UI.
737
738 Returns a hash reference with a single key, 'error'.  If there is an error,   
739 the value contains the error, otherwise it is empty. Takes a list of keys and
740 values as parameters with the following keys:
741
742 =over 4
743
744 =item secret
745
746 API Secret (required)
747
748 =item custnum
749
750 Customer number (required)
751
752 =back
753
754 =cut
755
756 sub bill_now {
757   my( $class, %opt ) = @_;
758   return _shared_secret_error() unless _check_shared_secret($opt{secret});
759
760   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
761     or return { 'error' => 'Unknown custnum' };
762
763   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
764                                             'retry'      => 1,
765                                             'check_freq' =>'1d',
766                                           );
767
768    return { 'error' => $error,
769           };
770
771 }
772
773
774 #next.. Delete Advertising sources?
775
776 =item list_advertising_sources OPTION => VALUE, ...
777
778 Lists all advertising sources.
779
780 =over
781
782 =item secret
783
784 API Secret
785
786 =back
787
788 Example:
789
790   my $result = FS::API->list_advertising_sources(
791     'secret'  => 'sharingiscaring',
792   );
793
794   if ( $result->{'error'} ) {
795     die $result->{'error'};
796   } else {
797     # list advertising sources returns an array of hashes for sources.
798     print Dumper($result->{'sources'});
799   }
800
801 =cut
802
803 #list_advertising_sources
804 sub list_advertising_sources {
805   my( $class, %opt ) = @_;
806   return _shared_secret_error() unless _check_shared_secret($opt{secret});
807
808   my @sources = qsearch('part_referral', {}, '', "")
809     or return { 'error' => 'No referrals' };
810
811   my $return = {
812     'sources'       => [ map $_->hashref, @sources ],
813   };
814
815   $return;
816 }
817
818 =item add_advertising_source OPTION => VALUE, ...
819
820 Add a new advertising source.
821
822 =over
823
824 =item secret
825
826 API Secret
827
828 =item referral
829
830 Referral name
831
832 =item disabled
833
834 Referral disabled, Y for disabled or nothing for enabled
835
836 =item agentnum
837
838 Agent ID number
839
840 =item title
841
842 External referral ID
843
844 =back
845
846 Example:
847
848   my $result = FS::API->add_advertising_source(
849     'secret'     => 'sharingiscaring',
850     'referral'   => 'test referral',
851
852     #optional
853     'disabled'   => 'Y',
854     'agentnum'   => '2', #agent id number
855     'title'      => 'test title',
856   );
857
858   if ( $result->{'error'} ) {
859     die $result->{'error'};
860   } else {
861     # add_advertising_source returns new source upon success.
862     print Dumper($result);
863   }
864
865 =cut
866
867 #add_advertising_source
868 sub add_advertising_source {
869   my( $class, %opt ) = @_;
870   return _shared_secret_error() unless _check_shared_secret($opt{secret});
871
872   use FS::part_referral;
873
874   my $new_source = $opt{source};
875
876   my $source = new FS::part_referral $new_source;
877
878   my $error = $source->insert;
879
880   my $return = {$source->hash};
881   $return = { 'error' => $error, } if $error;
882
883   $return;
884 }
885
886 =item edit_advertising_source OPTION => VALUE, ...
887
888 Edit a advertising source.
889
890 =over
891
892 =item secret
893
894 API Secret
895
896 =item refnum
897
898 Referral number to edit
899
900 =item source
901
902 hash of edited source fields.
903
904 =over
905
906 =item referral
907
908 Referral name
909
910 =item disabled
911
912 Referral disabled, Y for disabled or nothing for enabled
913
914 =item agentnum
915
916 Agent ID number
917
918 =item title
919
920 External referral ID
921
922 =back
923
924 =back
925
926 Example:
927
928   my $result = FS::API->edit_advertising_source(
929     'secret'     => 'sharingiscaring',
930     'refnum'     => '4', # referral number to edit
931     'source'     => {
932        #optional
933        'referral'   => 'test referral',
934        'disabled'   => 'Y',
935        'agentnum'   => '2', #agent id number
936        'title'      => 'test title',
937     }
938   );
939
940   if ( $result->{'error'} ) {
941     die $result->{'error'};
942   } else {
943     # edit_advertising_source returns updated source upon success.
944     print Dumper($result);
945   }
946
947 =cut
948
949 #edit_advertising_source
950 sub edit_advertising_source {
951   my( $class, %opt ) = @_;
952   return _shared_secret_error() unless _check_shared_secret($opt{secret});
953
954   use FS::part_referral;
955
956   my $refnum = $opt{refnum};
957   my $source = $opt{source};
958
959   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
960   my $new = new FS::part_referral { $old->hash };
961
962   foreach my $key (keys %$source) {
963     $new->$key($source->{$key});
964   }
965
966   my $error = $new->replace;
967
968   my $return = {$new->hash};
969   $return = { 'error' => $error, } if $error;
970
971   $return;
972 }
973
974
975 ##
976 # helper subroutines
977 ##
978
979 sub _check_shared_secret {
980   shift eq FS::Conf->new->config('api_shared_secret');
981 }
982
983 sub _shared_secret_error {
984   return { 'error' => 'Incorrect shared secret' };
985 }
986
987 1;