71513: Card tokenization [v3 test tweak]
[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 location_info
728
729 Returns location specific information for the customer. Takes a list of keys
730 and values as paramters with the following keys: custnum, secret
731
732 =cut
733
734 #I also monitor for changes to the additional locations that are applied to
735 # packages, and would like for those to be exportable as well.  basically the
736 # location data passed with the custnum.
737
738 sub location_info {
739   my( $class, %opt ) = @_;
740   return _shared_secret_error() unless _check_shared_secret($opt{secret});
741
742   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
743
744   my %return = (
745     'error'           => '',
746     'locations'       => [ map $_->hashref, @cust_location ],
747   );
748
749   return \%return;
750 }
751
752 =item change_package_location
753
754 Updates package location. Takes a list of keys and values 
755 as paramters with the following keys: 
756
757 pkgnum
758
759 secret
760
761 locationnum - pass this, or the following keys (don't pass both)
762
763 locationname
764
765 address1
766
767 address2
768
769 city
770
771 county
772
773 state
774
775 zip
776
777 addr_clean
778
779 country
780
781 censustract
782
783 censusyear
784
785 location_type
786
787 location_number
788
789 location_kind
790
791 incorporated
792
793 On error, returns a hashref with an 'error' key.
794 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
795 containing the new values.
796
797 =cut
798
799 sub change_package_location {
800   my $class = shift;
801   my %opt  = @_;
802   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
803
804   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
805     or return { 'error' => 'Unknown pkgnum' };
806
807   my %changeopt;
808
809   foreach my $field ( qw(
810     locationnum
811     locationname
812     address1
813     address2
814     city
815     county
816     state
817     zip
818     addr_clean
819     country
820     censustract
821     censusyear
822     location_type
823     location_number
824     location_kind
825     incorporated
826   )) {
827     $changeopt{$field} = $opt{$field} if $opt{$field};
828   }
829
830   $cust_pkg->API_change(%changeopt);
831 }
832
833 =item bill_now OPTION => VALUE, ...
834
835 Bills a single customer now, in the same fashion as the "Bill now" link in the
836 UI.
837
838 Returns a hash reference with a single key, 'error'.  If there is an error,   
839 the value contains the error, otherwise it is empty. Takes a list of keys and
840 values as parameters with the following keys:
841
842 =over 4
843
844 =item secret
845
846 API Secret (required)
847
848 =item custnum
849
850 Customer number (required)
851
852 =back
853
854 =cut
855
856 sub bill_now {
857   my( $class, %opt ) = @_;
858   return _shared_secret_error() unless _check_shared_secret($opt{secret});
859
860   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
861     or return { 'error' => 'Unknown custnum' };
862
863   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
864                                             'retry'      => 1,
865                                             'check_freq' =>'1d',
866                                           );
867
868    return { 'error' => $error,
869           };
870
871 }
872
873
874 #next.. Advertising sources?
875
876
877 ##
878 # helper subroutines
879 ##
880
881 sub _check_shared_secret {
882   shift eq FS::Conf->new->config('api_shared_secret');
883 }
884
885 sub _shared_secret_error {
886   return { 'error' => 'Incorrect shared secret' };
887 }
888
889 1;