Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / API.pm
1 package FS::API;
2
3 use strict;
4 use FS::Conf;
5 use FS::Record qw( qsearch qsearchs );
6 use FS::cust_main;
7 use FS::cust_location;
8 use FS::cust_pay;
9 use FS::cust_credit;
10 use FS::cust_refund;
11 use FS::cust_pkg;
12
13 =head1 NAME
14
15 FS::API - Freeside backend API
16
17 =head1 SYNOPSIS
18
19   use FS::API;
20
21 =head1 DESCRIPTION
22
23 This module implements a backend API for advanced back-office integration.
24
25 In contrast to the self-service API, which authenticates an end-user and offers
26 functionality to that end user, the backend API performs a simple shared-secret
27 authentication and offers full, administrator functionality, enabling
28 integration with other back-office systems.  Only access this API from a secure 
29 network from other backoffice machines. DON'T use this API to create customer 
30 portal functionality.
31
32 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
33 the port by default, only allow access from back-office servers with the same
34 security precations as the Freeside server, and encrypt the communication
35 channel (for example, with an SSH tunnel or VPN) rather than accessing it
36 in plaintext.
37
38 =head1 METHODS
39
40 =over 4
41
42 =item insert_payment OPTION => VALUE, ...
43
44 Adds a new payment to a customers account. Takes a list of keys and values as
45 paramters with the following keys:
46
47 =over 4
48
49 =item secret
50
51 API Secret
52
53 =item custnum
54
55 Customer number
56
57 =item payby
58
59 Payment type
60
61 =item paid
62
63 Amount paid
64
65 =item _date
66
67 Option date for payment
68
69 =back
70
71 Example:
72
73   my $result = FS::API->insert_payment(
74     'secret'  => 'sharingiscaring',
75     'custnum' => 181318,
76     'payby'   => 'CASH',
77     'paid'    => '54.32',
78
79     #optional
80     '_date'   => 1397977200, #UNIX timestamp
81   );
82
83   if ( $result->{'error'} ) {
84     die $result->{'error'};
85   } else {
86     #payment was inserted
87     print "paynum ". $result->{'paynum'};
88   }
89
90 =cut
91
92 #enter cash payment
93 sub insert_payment {
94   my($class, %opt) = @_;
95   return _shared_secret_error() unless _check_shared_secret($opt{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   $class->_by_phonenum('insert_payment', %opt);
110 }
111
112 sub _by_phonenum {
113   my($class, $method, %opt) = @_;
114   return _shared_secret_error() unless _check_shared_secret($opt{secret});
115
116   my $phonenum = delete $opt{'phonenum'};
117
118   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
119     or return { 'error' => 'Unknown phonenum' };
120
121   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
122     or return { 'error' => 'Unlinked phonenum' };
123
124   $opt{'custnum'} = $cust_pkg->custnum;
125
126   $class->$method(%opt);
127 }
128
129 =item insert_credit OPTION => VALUE, ...
130
131 Adds a a credit to a customers account.  Takes a list of keys and values as
132 parameters with the following keys
133
134 =over 
135
136 =item secret
137
138 API Secret
139
140 =item custnum
141
142 customer number
143
144 =item amount
145
146 Amount of the credit
147
148 =item _date
149
150 The date the credit will be posted
151
152 =back
153
154 Example:
155
156   my $result = FS::API->insert_credit(
157     'secret'  => 'sharingiscaring',
158     'custnum' => 181318,
159     'amount'  => '54.32',
160
161     #optional
162     '_date'   => 1397977200, #UNIX timestamp
163   );
164
165   if ( $result->{'error'} ) {
166     die $result->{'error'};
167   } else {
168     #credit was inserted
169     print "crednum ". $result->{'crednum'};
170   }
171
172 =cut
173
174 #Enter credit
175 sub insert_credit {
176   my($class, %opt) = @_;
177   return _shared_secret_error() unless _check_shared_secret($opt{secret});
178
179   $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
180
181   #less "raw" than this?  we are the backoffice API, and aren't worried
182   # about version migration ala cust_main/cust_location here
183   my $cust_credit = new FS::cust_credit { %opt };
184   my $error = $cust_credit->insert;
185   return { 'error'  => $error,
186            'crednum' => $cust_credit->crednum,
187          };
188 }
189
190 # pass the phone number ( from svc_phone ) 
191 sub insert_credit_phonenum {
192   my($class, %opt) = @_;
193   $class->_by_phonenum('insert_credit', %opt);
194 }
195
196 =item apply_payments_and_credits
197
198 Applies payments and credits for this customer.  Takes a list of keys and
199 values as parameter with the following keys:
200
201 =over 4
202
203 =item secret
204
205 API secret
206
207 =item custnum
208
209 Customer number
210
211 =back
212
213 =cut
214
215 #apply payments and credits
216 sub apply_payments_and_credits {
217   my($class, %opt) = @_;
218   return _shared_secret_error() unless _check_shared_secret($opt{secret});
219
220   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
221     or return { 'error' => 'Unknown custnum' };
222
223   my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
224   return { 'error'  => $error, };
225 }
226
227 =item insert_refund OPTION => VALUE, ...
228
229 Adds a a credit to a customers account.  Takes a list of keys and values as
230 parmeters with the following keys: custnum, payby, refund
231
232 Example:
233
234   my $result = FS::API->insert_refund(
235     'secret'  => 'sharingiscaring',
236     'custnum' => 181318,
237     'payby'   => 'CASH',
238     'refund'  => '54.32',
239
240     #optional
241     '_date'   => 1397977200, #UNIX timestamp
242   );
243
244   if ( $result->{'error'} ) {
245     die $result->{'error'};
246   } else {
247     #refund was inserted
248     print "refundnum ". $result->{'crednum'};
249   }
250
251 =cut
252
253 #Enter cash refund.
254 sub insert_refund {
255   my($class, %opt) = @_;
256   return _shared_secret_error() unless _check_shared_secret($opt{secret});
257
258   # when github pull request #24 is merged,
259   #  will have to change over to default reasonnum like credit
260   # but until then, this will do
261   $opt{'reason'} ||= 'API refund';
262
263   #less "raw" than this?  we are the backoffice API, and aren't worried
264   # about version migration ala cust_main/cust_location here
265   my $cust_refund = new FS::cust_refund { %opt };
266   my $error = $cust_refund->insert;
267   return { 'error'     => $error,
268            'refundnum' => $cust_refund->refundnum,
269          };
270 }
271
272 # pass the phone number ( from svc_phone ) 
273 sub insert_refund_phonenum {
274   my($class, %opt) = @_;
275   $class->_by_phonenum('insert_refund', %opt);
276 }
277
278 #---
279
280 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
281 # figure out how to trigger something when those things change
282
283 # long-term: package changes?
284
285 =item new_customer OPTION => VALUE, ...
286
287 Creates a new customer. Takes a list of keys and values as parameters with the
288 following keys:
289
290 =over 4
291
292 =item secret
293
294 API Secret
295
296 =item first
297
298 first name (required)
299
300 =item last
301
302 last name (required)
303
304 =item ss
305
306 (not typically collected; mostly used for ACH transactions)
307
308 =item company
309
310 Company name
311
312 =item address1 (required)
313
314 Address line one
315
316 =item city (required)
317
318 City
319
320 =item county
321
322 County
323
324 =item state (required)
325
326 State
327
328 =item zip (required)
329
330 Zip or postal code
331
332 =item country
333
334 2 Digit Country Code
335
336 =item latitude
337
338 latitude
339
340 =item Longitude
341
342 longitude
343
344 =item geocode
345
346 Currently used for third party tax vendor lookups
347
348 =item censustract
349
350 Used for determining FCC 477 reporting
351
352 =item censusyear
353
354 Used for determining FCC 477 reporting
355
356 =item daytime
357
358 Daytime phone number
359
360 =item night
361
362 Evening phone number
363
364 =item fax
365
366 Fax number
367
368 =item mobile
369
370 Mobile number
371
372 =item invoicing_list
373
374 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),
375 postal_invoicing
376 Set to 1 to enable postal invoicing
377
378 =item referral_custnum
379
380 Referring customer number
381
382 =item salesnum
383
384 Sales person number
385
386 =item agentnum
387
388 Agent number
389
390 =item agent_custid
391
392 Agent specific customer number
393
394 =item referral_custnum
395
396 Referring customer number
397
398 =back
399
400 =cut
401
402 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
403 # but approaching this from a clean start / back-office perspective
404 #  i.e. no package/service, no immediate credit card run, etc.
405
406 sub new_customer {
407   my( $class, %opt ) = @_;
408   return _shared_secret_error() unless _check_shared_secret($opt{secret});
409
410   #default agentnum like signup_server-default_agentnum?
411   #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
412  
413   #same for refnum like signup_server-default_refnum
414   $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
415
416   $class->API_insert( %opt );
417 }
418
419 =item update_customer
420
421 Updates an existing customer. Passing an empty value clears that field, while
422 NOT passing that key/value at all leaves it alone. Takes a list of keys and
423 values as parameters with the following keys:
424
425 =over 4
426
427 =item secret
428
429 API Secret (required)
430
431 =item custnum
432
433 Customer number (required)
434
435 =item first
436
437 first name 
438
439 =item last
440
441 last name 
442
443 =item company
444
445 Company name
446
447 =item address1 
448
449 Address line one
450
451 =item city 
452
453 City
454
455 =item county
456
457 County
458
459 =item state 
460
461 State
462
463 =item zip 
464
465 Zip or postal code
466
467 =item country
468
469 2 Digit Country Code
470
471 =item daytime
472
473 Daytime phone number
474
475 =item night
476
477 Evening phone number
478
479 =item fax
480
481 Fax number
482
483 =item mobile
484
485 Mobile number
486
487 =item invoicing_list
488
489 Comma-separated list of email addresses for email invoices. The special value 
490 'POST' is used to designate postal invoicing (it may be specified alone or in
491 addition to email addresses),
492 postal_invoicing
493 Set to 1 to enable postal invoicing
494
495 =item referral_custnum
496
497 Referring customer number
498
499 =item salesnum
500
501 Sales person number
502
503 =item agentnum
504
505 Agent number
506
507 =back
508
509 =cut
510
511 sub update_customer {
512   my( $class, %opt ) = @_;
513   return _shared_secret_error() unless _check_shared_secret($opt{secret});
514
515   FS::cust_main->API_update( %opt );
516 }
517
518 =item customer_info OPTION => VALUE, ...
519
520 Returns general customer information. Takes a list of keys and values as
521 parameters with the following keys: custnum, secret 
522
523 =cut
524
525 sub customer_info {
526   my( $class, %opt ) = @_;
527   return _shared_secret_error() unless _check_shared_secret($opt{secret});
528
529   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
530     or return { 'error' => 'Unknown custnum' };
531
532   $cust_main->API_getinfo;
533 }
534
535 =item location_info
536
537 Returns location specific information for the customer. Takes a list of keys
538 and values as paramters with the following keys: custnum, secret
539
540 =cut
541
542 #I also monitor for changes to the additional locations that are applied to
543 # packages, and would like for those to be exportable as well.  basically the
544 # location data passed with the custnum.
545
546 sub location_info {
547   my( $class, %opt ) = @_;
548   return _shared_secret_error() unless _check_shared_secret($opt{secret});
549
550   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
551
552   my %return = (
553     'error'           => '',
554     'locations'       => [ map $_->hashref, @cust_location ],
555   );
556
557   return \%return;
558 }
559
560 =item change_package_location
561
562 Updates package location. Takes a list of keys and values 
563 as paramters with the following keys: 
564
565 pkgnum
566
567 secret
568
569 locationnum - pass this, or the following keys (don't pass both)
570
571 locationname
572
573 address1
574
575 address2
576
577 city
578
579 county
580
581 state
582
583 zip
584
585 addr_clean
586
587 country
588
589 censustract
590
591 censusyear
592
593 location_type
594
595 location_number
596
597 location_kind
598
599 incorporated
600
601 On error, returns a hashref with an 'error' key.
602 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
603 containing the new values.
604
605 =cut
606
607 sub change_package_location {
608   my $class = shift;
609   my %opt  = @_;
610   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
611
612   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
613     or return { 'error' => 'Unknown pkgnum' };
614
615   my %changeopt;
616
617   foreach my $field ( qw(
618     locationnum
619     locationname
620     address1
621     address2
622     city
623     county
624     state
625     zip
626     addr_clean
627     country
628     censustract
629     censusyear
630     location_type
631     location_number
632     location_kind
633     incorporated
634   )) {
635     $changeopt{$field} = $opt{$field} if $opt{$field};
636   }
637
638   $cust_pkg->API_change(%changeopt);
639 }
640
641 =item bill_now OPTION => VALUE, ...
642
643 Bills a single customer now, in the same fashion as the "Bill now" link in the
644 UI.
645
646 Returns a hash reference with a single key, 'error'.  If there is an error,   
647 the value contains the error, otherwise it is empty. Takes a list of keys and
648 values as parameters with the following keys:
649
650 =over 4
651
652 =item secret
653
654 API Secret (required)
655
656 =item custnum
657
658 Customer number (required)
659
660 =back
661
662 =cut
663
664 sub bill_now {
665   my( $class, %opt ) = @_;
666   return _shared_secret_error() unless _check_shared_secret($opt{secret});
667
668   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
669     or return { 'error' => 'Unknown custnum' };
670
671   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
672                                             'retry'      => 1,
673                                             'check_freq' =>'1d',
674                                           );
675
676    return { 'error' => $error,
677           };
678
679 }
680
681
682 #next.. Advertising sources?
683
684
685 ##
686 # helper subroutines
687 ##
688
689 sub _check_shared_secret {
690   shift eq FS::Conf->new->config('api_shared_secret');
691 }
692
693 sub _shared_secret_error {
694   return { 'error' => 'Incorrect shared secret' };
695 }
696
697 1;