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