Fix backport #33252
[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.
27
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
32 in plaintext.
33
34 =head1 METHODS
35
36 =over 4
37
38 =item insert_payment OPTION => VALUE, ...
39
40 Adds a new payment to a customers account. Takes a list of keys and values as
41 paramters with the following keys:
42
43 =over 5
44
45 =item secret
46
47 API Secret
48
49 =item custnum
50
51 Customer number
52
53 =item payby
54
55 Payment type
56
57 =item paid
58
59 Amount paid
60
61 =item _date
62
63 Option date for payment
64
65 =back
66
67 Example:
68
69   my $result = FS::API->insert_payment(
70     'secret'  => 'sharingiscaring',
71     'custnum' => 181318,
72     'payby'   => 'CASH',
73     'paid'    => '54.32',
74
75     #optional
76     '_date'   => 1397977200, #UNIX timestamp
77   );
78
79   if ( $result->{'error'} ) {
80     die $result->{'error'};
81   } else {
82     #payment was inserted
83     print "paynum ". $result->{'paynum'};
84   }
85
86 =cut
87
88 #enter cash payment
89 sub insert_payment {
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');
94
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,
101          };
102 }
103
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');
110
111   $class->_by_phonenum('insert_payment', %opt);
112
113 }
114
115 sub _by_phonenum {
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');
120
121   my $phonenum = delete $opt{'phonenum'};
122
123   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
124     or return { 'error' => 'Unknown phonenum' };
125
126   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
127     or return { 'error' => 'Unlinked phonenum' };
128
129   $opt{'custnum'} = $cust_pkg->custnum;
130
131   $class->$method(%opt);
132
133 }
134
135 =item insert_credit OPTION => VALUE, ...
136
137 Adds a a credit to a customers account.  Takes a list of keys and values as
138 parameters with the following keys
139
140 =over 
141
142 =item secret
143
144 API Secret
145
146 =item custnum
147
148 customer number
149
150 =item amount
151
152 Amount of the credit
153
154 =item _date
155
156 The date the credit will be posted
157
158 =back
159
160 Example:
161
162   my $result = FS::API->insert_credit(
163     'secret'  => 'sharingiscaring',
164     'custnum' => 181318,
165     'amount'  => '54.32',
166
167     #optional
168     '_date'   => 1397977200, #UNIX timestamp
169   );
170
171   if ( $result->{'error'} ) {
172     die $result->{'error'};
173   } else {
174     #credit was inserted
175     print "crednum ". $result->{'crednum'};
176   }
177
178 =cut
179
180 #Enter credit
181 sub insert_credit {
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');
186
187   $opt{'reasonnum'} ||= $conf->config('api_credit_reason');
188
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,
195          };
196 }
197
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');
204
205   $class->_by_phonenum('insert_credit', %opt);
206
207 }
208
209 =item insert_refund OPTION => VALUE, ...
210
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
213
214 Example:
215
216   my $result = FS::API->insert_refund(
217     'secret'  => 'sharingiscaring',
218     'custnum' => 181318,
219     'payby'   => 'CASH',
220     'refund'  => '54.32',
221
222     #optional
223     '_date'   => 1397977200, #UNIX timestamp
224   );
225
226   if ( $result->{'error'} ) {
227     die $result->{'error'};
228   } else {
229     #refund was inserted
230     print "refundnum ". $result->{'crednum'};
231   }
232
233 =cut
234
235 #Enter cash refund.
236 sub insert_refund {
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');
241
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';
246
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,
253          };
254 }
255
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');
262
263   $class->_by_phonenum('insert_refund', %opt);
264
265 }
266
267 #---
268
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
271
272 # long-term: package changes?
273
274 =item new_customer OPTION => VALUE, ...
275
276 Creates a new customer. Takes a list of keys and values as parameters with the
277 following keys:
278
279 =over 4
280
281 =item secret
282
283 API Secret
284
285 =item first
286
287 first name (required)
288
289 =item last
290
291 last name (required)
292
293 =item ss
294
295 (not typically collected; mostly used for ACH transactions)
296
297 =item company
298
299 Company name
300
301 =item address1 (required)
302
303 Address line one
304
305 =item city (required)
306
307 City
308
309 =item county
310
311 County
312
313 =item state (required)
314
315 State
316
317 =item zip (required)
318
319 Zip or postal code
320
321 =item country
322
323 2 Digit Country Code
324
325 =item latitude
326
327 latitude
328
329 =item Longitude
330
331 longitude
332
333 =item geocode
334
335 Currently used for third party tax vendor lookups
336
337 =item censustract
338
339 Used for determining FCC 477 reporting
340
341 =item censusyear
342
343 Used for determining FCC 477 reporting
344
345 =item daytime
346
347 Daytime phone number
348
349 =item night
350
351 Evening phone number
352
353 =item fax
354
355 Fax number
356
357 =item mobile
358
359 Mobile number
360
361 =item invoicing_list
362
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),
364 postal_invoicing
365 Set to 1 to enable postal invoicing
366
367 =item payby
368
369 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
370
371 =item payinfo
372
373 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
374
375 =item paycvv
376
377 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
378
379 =item paydate
380
381 Expiration date for CARD/DCRD
382
383 =item payname
384
385 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
386
387 =item referral_custnum
388
389 Referring customer number
390
391 =item salesnum
392
393 Sales person number
394
395 =item agentnum
396
397 Agent number
398
399 =item agent_custid
400
401 Agent specific customer number
402
403 =item referral_custnum
404
405 Referring customer number
406
407 =back
408
409 =cut
410
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.
414
415 sub new_customer {
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');
420
421   #default agentnum like signup_server-default_agentnum?
422  
423   #same for refnum like signup_server-default_refnum
424
425   my $cust_main = new FS::cust_main ( {
426       'agentnum' => $agentnum,
427       'refnum'   => $opt{refnum}
428                     || $conf->config('signup_server-default_refnum'),
429       'payby'    => 'BILL',
430       'tagnum'   => [ FS::part_tag->default_tags ],
431
432       map { $_ => $opt{$_} } qw(
433         agentnum salesnum refnum agent_custid referral_custnum
434         last first company 
435         daytime night fax mobile
436         payby payinfo paydate paycvv payname
437       ),
438
439   } );
440
441   my @invoicing_list = $opt{'invoicing_list'}
442                          ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
443                          : ();
444   push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
445
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"};
451   }
452
453   my $bill_location = FS::cust_location->new($bill_hash);
454   my $ship_location;
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 
457   # address?
458   if ( length($ship_hash->{address1}) > 0 and
459           grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
460          ) {
461
462     $ship_location = FS::cust_location->new( $ship_hash );
463   
464   } else {
465     $ship_location = $bill_location;
466   }
467
468   $cust_main->set('bill_location' => $bill_location);
469   $cust_main->set('ship_location' => $ship_location);
470
471   $error = $cust_main->insert( {}, \@invoicing_list );
472   return { 'error'   => $error } if $error;
473   
474   return { 'error'   => '',
475            'custnum' => $cust_main->custnum,
476          };
477
478 }
479
480 =item update_customer
481
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:
485  
486 =over 4
487
488 =item secret
489
490 API Secret (required)
491
492 =item custnum
493
494 Customer number (required)
495
496 =item first
497
498 first name 
499
500 =item last
501
502 last name 
503
504 =item company
505
506 Company name
507
508 =item address1 
509
510 Address line one
511
512 =item city 
513
514 City
515
516 =item county
517
518 County
519
520 =item state 
521
522 State
523
524 =item zip 
525
526 Zip or postal code
527
528 =item country
529
530 2 Digit Country Code
531
532 =item daytime
533
534 Daytime phone number
535
536 =item night
537
538 Evening phone number
539
540 =item fax
541
542 Fax number
543
544 =item mobile
545
546 Mobile number
547
548 =item invoicing_list
549
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)
553
554 =item payby
555
556 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
557
558 =item payinfo
559
560 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid 
561 +"pin" for PREPAY, purchase order number for BILL
562
563 =item paycvv
564
565 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
566
567 =item paydate
568
569 Expiration date for CARD/DCRD
570
571 =item payname
572
573 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
574
575 =item referral_custnum
576
577 Referring customer number
578
579 =item salesnum
580
581 Sales person number
582
583 =item agentnum
584
585 Agent number
586
587 =back
588
589 =cut
590
591 sub update_customer {
592
593  my( $class, %opt ) = @_;
594
595   my $conf = new FS::Conf;
596   return { 'error' => 'Incorrect shared secret' }
597     unless $opt{secret} eq $conf->config('api_shared_secret');
598
599
600   my $custnum = $opt{'custnum'}
601     or return { 'error' => "no customer record" };
602
603   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
604     or return { 'error' => "unknown custnum $custnum" };
605
606   my $new = new FS::cust_main { $cust_main->hash };
607
608   $new->set( $_ => $opt{$_} )
609     foreach grep { exists $opt{$_} } qw(
610         agentnum salesnum refnum agent_custid referral_custnum
611         last first company
612         daytime night fax mobile
613         payby payinfo paydate paycvv payname
614       ),
615
616   my @invoicing_list;
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'};
620   } else {
621     @invoicing_list = $cust_main->invoicing_list;
622   }
623  
624   if ( exists( $opt{'address1'} ) ) {
625     my $bill_location = FS::cust_location->new({
626         map { $_ => $opt{$_} } @location_editable_fields
627     });
628     $bill_location->set('custnum' => $custnum);
629     my $error = $bill_location->find_or_insert;
630     die $error if $error;
631
632     # if this is unchanged from before, cust_main::replace will ignore it
633     $new->set('bill_location' => $bill_location);
634   }
635
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
639     });
640
641     $ship_location->set('custnum' => $custnum);
642     my $error = $ship_location->find_or_insert;
643     die $error if $error;
644
645     $new->set('ship_location' => $ship_location);
646
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);
650     }
651
652   my $error = $new->replace( $cust_main, \@invoicing_list );
653   return { 'error'   => $error } if $error;
654
655   return { 'error'   => '',
656          };  
657 }
658
659
660 =item customer_info
661
662 Returns general customer information. Takes a list of keys and values as
663 parameters with the following keys: custnum, secret 
664
665 =cut
666
667 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
668
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
672 );
673 #  locale
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
678 );
679
680 sub customer_info {
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');
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   my $conf = new FS::Conf;
736   return { 'error' => 'Incorrect shared secret' }
737     unless $opt{secret} eq $conf->config('api_shared_secret');
738
739   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
740
741   my %return = (
742     'error'           => '',
743     'locations'       => [ map $_->hashref, @cust_location ],
744   );
745
746   return \%return;
747 }
748
749 =item bill_now OPTION => VALUE, ...
750
751 Bills a single customer now, in the same fashion as the "Bill now" link in the
752 UI.
753
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:
757
758 =over 4
759
760 =item secret
761
762 API Secret (required)
763
764 =item custnum
765
766 Customer number (required)
767
768 =back
769
770 =cut
771
772 sub bill_now {
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');
777
778   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
779     or return { 'error' => 'Unknown custnum' };
780
781   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
782                                             'retry'      => 1,
783                                             'check_freq' =>'1d',
784                                           );
785
786    return { 'error' => $error,
787           };
788
789 }
790
791
792 #Advertising sources?
793
794
795 1;