4e6cb6cd73b1834acc65e67c03a5a2c02c36907b
[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 =back
69
70 Example:
71
72   my $result = FS::API->insert_payment(
73     'secret'  => 'sharingiscaring',
74     'custnum' => 181318,
75     'payby'   => 'CASH',
76     'paid'    => '54.32',
77
78     #optional
79     '_date'   => 1397977200, #UNIX timestamp
80   );
81
82   if ( $result->{'error'} ) {
83     die $result->{'error'};
84   } else {
85     #payment was inserted
86     print "paynum ". $result->{'paynum'};
87   }
88
89 =cut
90
91 #enter cash payment
92 sub insert_payment {
93   my($class, %opt) = @_;
94   return _shared_secret_error() unless _check_shared_secret($opt{secret});
95
96   #less "raw" than this?  we are the backoffice API, and aren't worried
97   # about version migration ala cust_main/cust_location here
98   my $cust_pay = new FS::cust_pay { %opt };
99   my $error = $cust_pay->insert( 'manual'=>1 );
100   return { 'error'  => $error,
101            'paynum' => $cust_pay->paynum,
102          };
103 }
104
105 # pass the phone number ( from svc_phone ) 
106 sub insert_payment_phonenum {
107   my($class, %opt) = @_;
108   $class->_by_phonenum('insert_payment', %opt);
109 }
110
111 sub _by_phonenum {
112   my($class, $method, %opt) = @_;
113   return _shared_secret_error() unless _check_shared_secret($opt{secret});
114
115   my $phonenum = delete $opt{'phonenum'};
116
117   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
118     or return { 'error' => 'Unknown phonenum' };
119
120   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
121     or return { 'error' => 'Unlinked phonenum' };
122
123   $opt{'custnum'} = $cust_pkg->custnum;
124
125   $class->$method(%opt);
126 }
127
128 =item insert_credit OPTION => VALUE, ...
129
130 Adds a a credit to a customers account.  Takes a list of keys and values as
131 parameters with the following keys
132
133 =over 
134
135 =item secret
136
137 API Secret
138
139 =item custnum
140
141 customer number
142
143 =item amount
144
145 Amount of the credit
146
147 =item _date
148
149 The date the credit will be posted
150
151 =back
152
153 Example:
154
155   my $result = FS::API->insert_credit(
156     'secret'  => 'sharingiscaring',
157     'custnum' => 181318,
158     'amount'  => '54.32',
159
160     #optional
161     '_date'   => 1397977200, #UNIX timestamp
162   );
163
164   if ( $result->{'error'} ) {
165     die $result->{'error'};
166   } else {
167     #credit was inserted
168     print "crednum ". $result->{'crednum'};
169   }
170
171 =cut
172
173 #Enter credit
174 sub insert_credit {
175   my($class, %opt) = @_;
176   return _shared_secret_error() unless _check_shared_secret($opt{secret});
177
178   $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
179
180   #less "raw" than this?  we are the backoffice API, and aren't worried
181   # about version migration ala cust_main/cust_location here
182   my $cust_credit = new FS::cust_credit { %opt };
183   my $error = $cust_credit->insert;
184   return { 'error'  => $error,
185            'crednum' => $cust_credit->crednum,
186          };
187 }
188
189 # pass the phone number ( from svc_phone ) 
190 sub insert_credit_phonenum {
191   my($class, %opt) = @_;
192   $class->_by_phonenum('insert_credit', %opt);
193 }
194
195 =item apply_payments_and_credits
196
197 Applies payments and credits for this customer.  Takes a list of keys and
198 values as parameter with the following keys:
199
200 =over 4
201
202 =item secret
203
204 API secret
205
206 =item custnum
207
208 Customer number
209
210 =back
211
212 =cut
213
214 #apply payments and credits
215 sub apply_payments_and_credits {
216   my($class, %opt) = @_;
217   return _shared_secret_error() unless _check_shared_secret($opt{secret});
218
219   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
220     or return { 'error' => 'Unknown custnum' };
221
222   my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
223   return { 'error'  => $error, };
224 }
225
226 =item insert_refund OPTION => VALUE, ...
227
228 Adds a a credit to a customers account.  Takes a list of keys and values as
229 parmeters with the following keys: custnum, payby, refund
230
231 Example:
232
233   my $result = FS::API->insert_refund(
234     'secret'  => 'sharingiscaring',
235     'custnum' => 181318,
236     'payby'   => 'CASH',
237     'refund'  => '54.32',
238
239     #optional
240     '_date'   => 1397977200, #UNIX timestamp
241   );
242
243   if ( $result->{'error'} ) {
244     die $result->{'error'};
245   } else {
246     #refund was inserted
247     print "refundnum ". $result->{'crednum'};
248   }
249
250 =cut
251
252 #Enter cash refund.
253 sub insert_refund {
254   my($class, %opt) = @_;
255   return _shared_secret_error() unless _check_shared_secret($opt{secret});
256
257   # when github pull request #24 is merged,
258   #  will have to change over to default reasonnum like credit
259   # but until then, this will do
260   $opt{'reason'} ||= 'API refund';
261
262   #less "raw" than this?  we are the backoffice API, and aren't worried
263   # about version migration ala cust_main/cust_location here
264   my $cust_refund = new FS::cust_refund { %opt };
265   my $error = $cust_refund->insert;
266   return { 'error'     => $error,
267            'refundnum' => $cust_refund->refundnum,
268          };
269 }
270
271 # pass the phone number ( from svc_phone ) 
272 sub insert_refund_phonenum {
273   my($class, %opt) = @_;
274   $class->_by_phonenum('insert_refund', %opt);
275 }
276
277 #---
278
279 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
280 # figure out how to trigger something when those things change
281
282 # long-term: package changes?
283
284 =item new_customer OPTION => VALUE, ...
285
286 Creates a new customer. Takes a list of keys and values as parameters with the
287 following keys:
288
289 =over 4
290
291 =item secret
292
293 API Secret
294
295 =item first
296
297 first name (required)
298
299 =item last
300
301 last name (required)
302
303 =item ss
304
305 (not typically collected; mostly used for ACH transactions)
306
307 =item company
308
309 Company name
310
311 =item address1 (required)
312
313 Address line one
314
315 =item city (required)
316
317 City
318
319 =item county
320
321 County
322
323 =item state (required)
324
325 State
326
327 =item zip (required)
328
329 Zip or postal code
330
331 =item country
332
333 2 Digit Country Code
334
335 =item latitude
336
337 latitude
338
339 =item Longitude
340
341 longitude
342
343 =item geocode
344
345 Currently used for third party tax vendor lookups
346
347 =item censustract
348
349 Used for determining FCC 477 reporting
350
351 =item censusyear
352
353 Used for determining FCC 477 reporting
354
355 =item daytime
356
357 Daytime phone number
358
359 =item night
360
361 Evening phone number
362
363 =item fax
364
365 Fax number
366
367 =item mobile
368
369 Mobile number
370
371 =item invoicing_list
372
373 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 postal_invoicing
375 Set to 1 to enable postal invoicing
376
377 =item payby
378
379 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
380
381 =item payinfo
382
383 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
384
385 =item paycvv
386
387 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
388
389 =item paydate
390
391 Expiration date for CARD/DCRD
392
393 =item payname
394
395 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
396
397 =item referral_custnum
398
399 Referring customer number
400
401 =item salesnum
402
403 Sales person number
404
405 =item agentnum
406
407 Agent number
408
409 =item agent_custid
410
411 Agent specific customer number
412
413 =item referral_custnum
414
415 Referring customer number
416
417 =back
418
419 =cut
420
421 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
422 # but approaching this from a clean start / back-office perspective
423 #  i.e. no package/service, no immediate credit card run, etc.
424
425 sub new_customer {
426   my( $class, %opt ) = @_;
427   return _shared_secret_error() unless _check_shared_secret($opt{secret});
428
429   #default agentnum like signup_server-default_agentnum?
430  
431   #same for refnum like signup_server-default_refnum
432
433   my $cust_main = new FS::cust_main ( {
434       'refnum'   => $opt{refnum}
435                     || FS::Conf->new->config('signup_server-default_refnum'),
436       'payby'    => 'BILL',
437       'tagnum'   => [ FS::part_tag->default_tags ],
438
439       map { $_ => $opt{$_} } qw(
440         agentnum salesnum refnum agent_custid referral_custnum
441         last first company 
442         daytime night fax mobile
443         payby payinfo paydate paycvv payname
444       ),
445
446   } );
447
448   my @invoicing_list = $opt{'invoicing_list'}
449                          ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
450                          : ();
451   push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
452
453   my ($bill_hash, $ship_hash);
454   foreach my $f (FS::cust_main->location_fields) {
455     # avoid having to change this in front-end code
456     $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
457     $ship_hash->{$f} = $opt{"ship_$f"};
458   }
459
460   my $bill_location = FS::cust_location->new($bill_hash);
461   my $ship_location;
462   # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
463   # so is there a ship address, and if so, is it different from the billing 
464   # address?
465   if ( length($ship_hash->{address1}) > 0 and
466           grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
467          ) {
468
469     $ship_location = FS::cust_location->new( $ship_hash );
470   
471   } else {
472     $ship_location = $bill_location;
473   }
474
475   $cust_main->set('bill_location' => $bill_location);
476   $cust_main->set('ship_location' => $ship_location);
477
478   $error = $cust_main->insert( {}, \@invoicing_list );
479   return { 'error'   => $error } if $error;
480   
481   return { 'error'   => '',
482            'custnum' => $cust_main->custnum,
483          };
484
485 }
486
487 =item update_customer
488
489 Updates an existing customer. Passing an empty value clears that field, while
490 NOT passing that key/value at all leaves it alone. Takes a list of keys and
491 values as parameters with the following keys:
492  
493 =over 4
494
495 =item secret
496
497 API Secret (required)
498
499 =item custnum
500
501 Customer number (required)
502
503 =item first
504
505 first name 
506
507 =item last
508
509 last name 
510
511 =item company
512
513 Company name
514
515 =item address1 
516
517 Address line one
518
519 =item city 
520
521 City
522
523 =item county
524
525 County
526
527 =item state 
528
529 State
530
531 =item zip 
532
533 Zip or postal code
534
535 =item country
536
537 2 Digit Country Code
538
539 =item daytime
540
541 Daytime phone number
542
543 =item night
544
545 Evening phone number
546
547 =item fax
548
549 Fax number
550
551 =item mobile
552
553 Mobile number
554
555 =item invoicing_list
556
557 Comma-separated list of email addresses for email invoices. The special value 
558 'POST' is used to designate postal invoicing (it may be specified alone or in
559 addition to email addresses)
560
561 =item payby
562
563 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
564
565 =item payinfo
566
567 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid 
568 +"pin" for PREPAY, purchase order number for BILL
569
570 =item paycvv
571
572 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
573
574 =item paydate
575
576 Expiration date for CARD/DCRD
577
578 =item payname
579
580 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
581
582 =item referral_custnum
583
584 Referring customer number
585
586 =item salesnum
587
588 Sales person number
589
590 =item agentnum
591
592 Agent number
593
594 =back
595
596 =cut
597
598 sub update_customer {
599  my( $class, %opt ) = @_;
600   return _shared_secret_error() unless _check_shared_secret($opt{secret});
601
602   my $custnum = $opt{'custnum'}
603     or return { 'error' => "no customer record" };
604
605   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
606     or return { 'error' => "unknown custnum $custnum" };
607
608   my $new = new FS::cust_main { $cust_main->hash };
609
610   $new->set( $_ => $opt{$_} )
611     foreach grep { exists $opt{$_} } qw(
612         agentnum salesnum refnum agent_custid referral_custnum
613         last first company
614         daytime night fax mobile
615         payby payinfo paydate paycvv payname
616       ),
617
618   my @invoicing_list;
619   if ( exists $opt{'invoicing_list'} || exists $opt{'postal_invoicing'} ) {
620     @invoicing_list = split( /\s*\,\s*/, $opt{'invoicing_list'} );
621     push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
622   } else {
623     @invoicing_list = $cust_main->invoicing_list;
624   }
625  
626   if ( exists( $opt{'address1'} ) ) {
627     my $bill_location = FS::cust_location->new({
628         map { $_ => $opt{$_} } @location_editable_fields
629     });
630     $bill_location->set('custnum' => $custnum);
631     my $error = $bill_location->find_or_insert;
632     die $error if $error;
633
634     # if this is unchanged from before, cust_main::replace will ignore it
635     $new->set('bill_location' => $bill_location);
636   }
637
638   if ( exists($opt{'ship_address1'}) && length($opt{"ship_address1"}) > 0 ) {
639     my $ship_location = FS::cust_location->new({
640         map { $_ => $opt{"ship_$_"} } @location_editable_fields
641     });
642
643     $ship_location->set('custnum' => $custnum);
644     my $error = $ship_location->find_or_insert;
645     die $error if $error;
646
647     $new->set('ship_location' => $ship_location);
648
649    } elsif (exists($opt{'ship_address1'} ) && !grep { length($opt{"ship_$_"}) } @location_editable_fields ) {
650       my $ship_location = $new->bill_location;
651      $new->set('ship_location' => $ship_location);
652     }
653
654   my $error = $new->replace( $cust_main, \@invoicing_list );
655   return { 'error'   => $error } if $error;
656
657   return { 'error'   => '',
658          };  
659 }
660
661
662 =item customer_info
663
664 Returns general customer information. Takes a list of keys and values as
665 parameters with the following keys: custnum, secret 
666
667 =cut
668
669 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
670
671 use vars qw( @cust_main_editable_fields @location_editable_fields );
672 @cust_main_editable_fields = qw(
673   first last company daytime night fax mobile
674 );
675 #  locale
676 #  payby payinfo payname paystart_month paystart_year payissue payip
677 #  ss paytype paystate stateid stateid_state
678 @location_editable_fields = qw(
679   address1 address2 city county state zip country
680 );
681
682 sub customer_info {
683   my( $class, %opt ) = @_;
684   return _shared_secret_error() unless _check_shared_secret($opt{secret});
685
686   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
687     or return { 'error' => 'Unknown custnum' };
688
689   my %return = (
690     'error'           => '',
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,
696   );
697
698   $return{$_} = $cust_main->get($_)
699     foreach @cust_main_editable_fields;
700
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;
706   }
707
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 );
713
714   #generally, the more useful data from the cust_main record the better.
715   # well, tell me what you want
716
717   return \%return;
718
719 }
720
721
722 =item location_info
723
724 Returns location specific information for the customer. Takes a list of keys
725 and values as paramters with the following keys: custnum, secret
726
727 =cut
728
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.
732
733 sub location_info {
734   my( $class, %opt ) = @_;
735   return _shared_secret_error() unless _check_shared_secret($opt{secret});
736
737   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
738
739   my %return = (
740     'error'           => '',
741     'locations'       => [ map $_->hashref, @cust_location ],
742   );
743
744   return \%return;
745 }
746
747 =item change_package_location
748
749 Updates package location. Takes a list of keys and values 
750 as paramters with the following keys: 
751
752 pkgnum
753
754 secret
755
756 locationnum - pass this, or the following keys (don't pass both)
757
758 locationname
759
760 address1
761
762 address2
763
764 city
765
766 county
767
768 state
769
770 zip
771
772 addr_clean
773
774 country
775
776 censustract
777
778 censusyear
779
780 location_type
781
782 location_number
783
784 location_kind
785
786 incorporated
787
788 On error, returns a hashref with an 'error' key.
789 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
790 containing the new values.
791
792 =cut
793
794 sub change_package_location {
795   my $class = shift;
796   my %opt  = @_;
797   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
798
799   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
800     or return { 'error' => 'Unknown pkgnum' };
801
802   my %changeopt;
803
804   foreach my $field ( qw(
805     locationnum
806     locationname
807     address1
808     address2
809     city
810     county
811     state
812     zip
813     addr_clean
814     country
815     censustract
816     censusyear
817     location_type
818     location_number
819     location_kind
820     incorporated
821   )) {
822     $changeopt{$field} = $opt{$field} if $opt{$field};
823   }
824
825   $cust_pkg->API_change(%changeopt);
826 }
827
828 =item bill_now OPTION => VALUE, ...
829
830 Bills a single customer now, in the same fashion as the "Bill now" link in the
831 UI.
832
833 Returns a hash reference with a single key, 'error'.  If there is an error,   
834 the value contains the error, otherwise it is empty. Takes a list of keys and
835 values as parameters with the following keys:
836
837 =over 4
838
839 =item secret
840
841 API Secret (required)
842
843 =item custnum
844
845 Customer number (required)
846
847 =back
848
849 =cut
850
851 sub bill_now {
852   my( $class, %opt ) = @_;
853   return _shared_secret_error() unless _check_shared_secret($opt{secret});
854
855   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
856     or return { 'error' => 'Unknown custnum' };
857
858   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
859                                             'retry'      => 1,
860                                             'check_freq' =>'1d',
861                                           );
862
863    return { 'error' => $error,
864           };
865
866 }
867
868
869 #next.. Advertising sources?
870
871
872 ##
873 # helper subroutines
874 ##
875
876 sub _check_shared_secret {
877   shift eq FS::Conf->new->config('api_shared_secret');
878 }
879
880 sub _shared_secret_error {
881   return { 'error' => 'Incorrect shared secret' };
882 }
883
884 1;