37669 Fix typo
[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
11 =head1 NAME
12
13 FS::API - Freeside backend API
14
15 =head1 SYNOPSIS
16
17   use FS::API;
18
19 =head1 DESCRIPTION
20
21 This module implements a backend API for advanced back-office integration.
22
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 
28 portal functionality.
29
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
34 in plaintext.
35
36 =head1 METHODS
37
38 =over 4
39
40 =item insert_payment OPTION => VALUE, ...
41
42 Adds a new payment to a customers account. Takes a list of keys and values as
43 paramters with the following keys:
44
45 =over 5
46
47 =item secret
48
49 API Secret
50
51 =item custnum
52
53 Customer number
54
55 =item payby
56
57 Payment type
58
59 =item paid
60
61 Amount paid
62
63 =item _date
64
65 Option date for payment
66
67 =back
68
69 Example:
70
71   my $result = FS::API->insert_payment(
72     'secret'  => 'sharingiscaring',
73     'custnum' => 181318,
74     'payby'   => 'CASH',
75     'paid'    => '54.32',
76
77     #optional
78     '_date'   => 1397977200, #UNIX timestamp
79   );
80
81   if ( $result->{'error'} ) {
82     die $result->{'error'};
83   } else {
84     #payment was inserted
85     print "paynum ". $result->{'paynum'};
86   }
87
88 =cut
89
90 #enter cash payment
91 sub insert_payment {
92   my($class, %opt) = @_;
93   my $conf = new FS::Conf;
94   return { 'error' => 'Incorrect shared secret' }
95     unless $opt{secret} eq $conf->config('api_shared_secret');
96
97   #less "raw" than this?  we are the backoffice API, and aren't worried
98   # about version migration ala cust_main/cust_location here
99   my $cust_pay = new FS::cust_pay { %opt };
100   my $error = $cust_pay->insert( 'manual'=>1 );
101   return { 'error'  => $error,
102            'paynum' => $cust_pay->paynum,
103          };
104 }
105
106 # pass the phone number ( from svc_phone ) 
107 sub insert_payment_phonenum {
108   my($class, %opt) = @_;
109   my $conf = new FS::Conf;
110   return { 'error' => 'Incorrect shared secret' }
111     unless $opt{secret} eq $conf->config('api_shared_secret');
112
113   $class->_by_phonenum('insert_payment', %opt);
114
115 }
116
117 sub _by_phonenum {
118   my($class, $method, %opt) = @_;
119   my $conf = new FS::Conf;
120   return { 'error' => 'Incorrect shared secret' }
121     unless $opt{secret} eq $conf->config('api_shared_secret');
122
123   my $phonenum = delete $opt{'phonenum'};
124
125   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
126     or return { 'error' => 'Unknown phonenum' };
127
128   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
129     or return { 'error' => 'Unlinked phonenum' };
130
131   $opt{'custnum'} = $cust_pkg->custnum;
132
133   $class->$method(%opt);
134
135 }
136
137 =item insert_credit OPTION => VALUE, ...
138
139 Adds a a credit to a customers account.  Takes a list of keys and values as
140 parameters with the following keys
141
142 =over 
143
144 =item secret
145
146 API Secret
147
148 =item custnum
149
150 customer number
151
152 =item amount
153
154 Amount of the credit
155
156 =item _date
157
158 The date the credit will be posted
159
160 =back
161
162 Example:
163
164   my $result = FS::API->insert_credit(
165     'secret'  => 'sharingiscaring',
166     'custnum' => 181318,
167     'amount'  => '54.32',
168
169     #optional
170     '_date'   => 1397977200, #UNIX timestamp
171   );
172
173   if ( $result->{'error'} ) {
174     die $result->{'error'};
175   } else {
176     #credit was inserted
177     print "crednum ". $result->{'crednum'};
178   }
179
180 =cut
181
182 #Enter credit
183 sub insert_credit {
184   my($class, %opt) = @_;
185   my $conf = new FS::Conf;
186   return { 'error' => 'Incorrect shared secret' }
187     unless $opt{secret} eq $conf->config('api_shared_secret');
188
189   $opt{'reasonnum'} ||= $conf->config('api_credit_reason');
190
191   #less "raw" than this?  we are the backoffice API, and aren't worried
192   # about version migration ala cust_main/cust_location here
193   my $cust_credit = new FS::cust_credit { %opt };
194   my $error = $cust_credit->insert;
195   return { 'error'  => $error,
196            'crednum' => $cust_credit->crednum,
197          };
198 }
199
200 # pass the phone number ( from svc_phone ) 
201 sub insert_credit_phonenum {
202   my($class, %opt) = @_;
203   my $conf = new FS::Conf;
204   return { 'error' => 'Incorrect shared secret' }
205     unless $opt{secret} eq $conf->config('api_shared_secret');
206
207   $class->_by_phonenum('insert_credit', %opt);
208
209 }
210
211 =item insert_refund OPTION => VALUE, ...
212
213 Adds a a credit to a customers account.  Takes a list of keys and values as
214 parmeters with the following keys: custnum, payby, refund
215
216 Example:
217
218   my $result = FS::API->insert_refund(
219     'secret'  => 'sharingiscaring',
220     'custnum' => 181318,
221     'payby'   => 'CASH',
222     'refund'  => '54.32',
223
224     #optional
225     '_date'   => 1397977200, #UNIX timestamp
226   );
227
228   if ( $result->{'error'} ) {
229     die $result->{'error'};
230   } else {
231     #refund was inserted
232     print "refundnum ". $result->{'crednum'};
233   }
234
235 =cut
236
237 #Enter cash refund.
238 sub insert_refund {
239   my($class, %opt) = @_;
240   my $conf = new FS::Conf;
241   return { 'error' => 'Incorrect shared secret' }
242     unless $opt{secret} eq $conf->config('api_shared_secret');
243
244   # when github pull request #24 is merged,
245   #  will have to change over to default reasonnum like credit
246   # but until then, this will do
247   $opt{'reason'} ||= 'API refund';
248
249   #less "raw" than this?  we are the backoffice API, and aren't worried
250   # about version migration ala cust_main/cust_location here
251   my $cust_refund = new FS::cust_refund { %opt };
252   my $error = $cust_refund->insert;
253   return { 'error'     => $error,
254            'refundnum' => $cust_refund->refundnum,
255          };
256 }
257
258 # pass the phone number ( from svc_phone ) 
259 sub insert_refund_phonenum {
260   my($class, %opt) = @_;
261   my $conf = new FS::Conf;
262   return { 'error' => 'Incorrect shared secret' }
263     unless $opt{secret} eq $conf->config('api_shared_secret');
264
265   $class->_by_phonenum('insert_refund', %opt);
266
267 }
268
269 #---
270
271 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
272 # figure out how to trigger something when those things change
273
274 # long-term: package changes?
275
276 =item new_customer OPTION => VALUE, ...
277
278 Creates a new customer. Takes a list of keys and values as parameters with the
279 following keys:
280
281 =over 4
282
283 =item secret
284
285 API Secret
286
287 =item first
288
289 first name (required)
290
291 =item last
292
293 last name (required)
294
295 =item ss
296
297 (not typically collected; mostly used for ACH transactions)
298
299 =item company
300
301 Company name
302
303 =item address1 (required)
304
305 Address line one
306
307 =item city (required)
308
309 City
310
311 =item county
312
313 County
314
315 =item state (required)
316
317 State
318
319 =item zip (required)
320
321 Zip or postal code
322
323 =item country
324
325 2 Digit Country Code
326
327 =item latitude
328
329 latitude
330
331 =item Longitude
332
333 longitude
334
335 =item geocode
336
337 Currently used for third party tax vendor lookups
338
339 =item censustract
340
341 Used for determining FCC 477 reporting
342
343 =item censusyear
344
345 Used for determining FCC 477 reporting
346
347 =item daytime
348
349 Daytime phone number
350
351 =item night
352
353 Evening phone number
354
355 =item fax
356
357 Fax number
358
359 =item mobile
360
361 Mobile number
362
363 =item invoicing_list
364
365 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),
366 postal_invoicing
367 Set to 1 to enable postal invoicing
368
369 =item payby
370
371 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
372
373 =item payinfo
374
375 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
376
377 =item paycvv
378
379 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
380
381 =item paydate
382
383 Expiration date for CARD/DCRD
384
385 =item payname
386
387 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
388
389 =item referral_custnum
390
391 Referring customer number
392
393 =item salesnum
394
395 Sales person number
396
397 =item agentnum
398
399 Agent number
400
401 =item agent_custid
402
403 Agent specific customer number
404
405 =item referral_custnum
406
407 Referring customer number
408
409 =back
410
411 =cut
412
413 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
414 # but approaching this from a clean start / back-office perspective
415 #  i.e. no package/service, no immediate credit card run, etc.
416
417 sub new_customer {
418   my( $class, %opt ) = @_;
419   my $conf = new FS::Conf;
420   return { 'error' => 'Incorrect shared secret' }
421     unless $opt{secret} eq $conf->config('api_shared_secret');
422
423   #default agentnum like signup_server-default_agentnum?
424  
425   #same for refnum like signup_server-default_refnum
426
427   my $cust_main = new FS::cust_main ( {
428       'agentnum' => $agentnum,
429       'refnum'   => $opt{refnum}
430                     || $conf->config('signup_server-default_refnum'),
431       'payby'    => 'BILL',
432       'tagnum'   => [ FS::part_tag->default_tags ],
433
434       map { $_ => $opt{$_} } qw(
435         agentnum salesnum refnum agent_custid referral_custnum
436         last first company 
437         daytime night fax mobile
438         payby payinfo paydate paycvv payname
439       ),
440
441   } );
442
443   my @invoicing_list = $opt{'invoicing_list'}
444                          ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
445                          : ();
446   push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
447
448   my ($bill_hash, $ship_hash);
449   foreach my $f (FS::cust_main->location_fields) {
450     # avoid having to change this in front-end code
451     $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
452     $ship_hash->{$f} = $opt{"ship_$f"};
453   }
454
455   my $bill_location = FS::cust_location->new($bill_hash);
456   my $ship_location;
457   # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
458   # so is there a ship address, and if so, is it different from the billing 
459   # address?
460   if ( length($ship_hash->{address1}) > 0 and
461           grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
462          ) {
463
464     $ship_location = FS::cust_location->new( $ship_hash );
465   
466   } else {
467     $ship_location = $bill_location;
468   }
469
470   $cust_main->set('bill_location' => $bill_location);
471   $cust_main->set('ship_location' => $ship_location);
472
473   $error = $cust_main->insert( {}, \@invoicing_list );
474   return { 'error'   => $error } if $error;
475   
476   return { 'error'   => '',
477            'custnum' => $cust_main->custnum,
478          };
479
480 }
481
482 =item update_customer
483
484 Updates an existing customer. Passing an empty value clears that field, while
485 NOT passing that key/value at all leaves it alone. Takes a list of keys and
486 values as parameters with the following keys:
487  
488 =over 4
489
490 =item secret
491
492 API Secret (required)
493
494 =item custnum
495
496 Customer number (required)
497
498 =item first
499
500 first name 
501
502 =item last
503
504 last name 
505
506 =item company
507
508 Company name
509
510 =item address1 
511
512 Address line one
513
514 =item city 
515
516 City
517
518 =item county
519
520 County
521
522 =item state 
523
524 State
525
526 =item zip 
527
528 Zip or postal code
529
530 =item country
531
532 2 Digit Country Code
533
534 =item daytime
535
536 Daytime phone number
537
538 =item night
539
540 Evening phone number
541
542 =item fax
543
544 Fax number
545
546 =item mobile
547
548 Mobile number
549
550 =item invoicing_list
551
552 Comma-separated list of email addresses for email invoices. The special value 
553 'POST' is used to designate postal invoicing (it may be specified alone or in
554 addition to email addresses)
555
556 =item payby
557
558 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
559
560 =item payinfo
561
562 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid 
563 +"pin" for PREPAY, purchase order number for BILL
564
565 =item paycvv
566
567 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
568
569 =item paydate
570
571 Expiration date for CARD/DCRD
572
573 =item payname
574
575 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
576
577 =item referral_custnum
578
579 Referring customer number
580
581 =item salesnum
582
583 Sales person number
584
585 =item agentnum
586
587 Agent number
588
589 =back
590
591 =cut
592
593 sub update_customer {
594
595  my( $class, %opt ) = @_;
596
597   my $conf = new FS::Conf;
598   return { 'error' => 'Incorrect shared secret' }
599     unless $opt{secret} eq $conf->config('api_shared_secret');
600
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   my $conf = new FS::Conf;
685   return { 'error' => 'Incorrect shared secret' }
686     unless $opt{secret} eq $conf->config('api_shared_secret');
687
688   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
689     or return { 'error' => 'Unknown custnum' };
690
691   my %return = (
692     'error'           => '',
693     'display_custnum' => $cust_main->display_custnum,
694     'name'            => $cust_main->first. ' '. $cust_main->get('last'),
695     'balance'         => $cust_main->balance,
696     'status'          => $cust_main->status,
697     'statuscolor'     => $cust_main->statuscolor,
698   );
699
700   $return{$_} = $cust_main->get($_)
701     foreach @cust_main_editable_fields;
702
703   for (@location_editable_fields) {
704     $return{$_} = $cust_main->bill_location->get($_)
705       if $cust_main->bill_locationnum;
706     $return{'ship_'.$_} = $cust_main->ship_location->get($_)
707       if $cust_main->ship_locationnum;
708   }
709
710   my @invoicing_list = $cust_main->invoicing_list;
711   $return{'invoicing_list'} =
712     join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
713   $return{'postal_invoicing'} =
714     0 < ( grep { $_ eq 'POST' } @invoicing_list );
715
716   #generally, the more useful data from the cust_main record the better.
717   # well, tell me what you want
718
719   return \%return;
720
721 }
722
723
724 =item location_info
725
726 Returns location specific information for the customer. Takes a list of keys
727 and values as paramters with the following keys: custnum, secret
728
729 =cut
730
731 #I also monitor for changes to the additional locations that are applied to
732 # packages, and would like for those to be exportable as well.  basically the
733 # location data passed with the custnum.
734
735 sub location_info {
736   my( $class, %opt ) = @_;
737   my $conf = new FS::Conf;
738   return { 'error' => 'Incorrect shared secret' }
739     unless $opt{secret} eq $conf->config('api_shared_secret');
740
741   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
742
743   my %return = (
744     'error'           => '',
745     'locations'       => [ map $_->hashref, @cust_location ],
746   );
747
748   return \%return;
749 }
750
751 =item bill_now OPTION => VALUE, ...
752
753 Bills a single customer now, in the same fashion as the "Bill now" link in the
754 UI.
755
756 Returns a hash reference with a single key, 'error'.  If there is an error,   
757 the value contains the error, otherwise it is empty. Takes a list of keys and
758 values as parameters with the following keys:
759
760 =over 4
761
762 =item secret
763
764 API Secret (required)
765
766 =item custnum
767
768 Customer number (required)
769
770 =back
771
772 =cut
773
774 sub bill_now {
775   my( $class, %opt ) = @_;
776   my $conf = new FS::Conf;
777   return { 'error' => 'Incorrect shared secret' }
778     unless $opt{secret} eq $conf->config('api_shared_secret');
779
780   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
781     or return { 'error' => 'Unknown custnum' };
782
783   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
784                                             'retry'      => 1,
785                                             'check_freq' =>'1d',
786                                           );
787
788    return { 'error' => $error,
789           };
790
791 }
792
793
794 #Advertising sources?
795
796
797 1;