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