RT# 27969 - created 3 new api functions to add, edit and list advertising sources
[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 customer_list_svcs OPTION => VALUE, ...
541
542 Returns customer service information.  Takes a list of keys and values as
543 parameters with the following keys: custnum, secret
544
545 =cut
546
547 sub customer_list_svcs {
548   my( $class, %opt ) = @_;
549   return _shared_secret_error() unless _check_shared_secret($opt{secret});
550
551   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
552     or return { 'error' => 'Unknown custnum' };
553
554   #$cust_main->API_list_svcs;
555
556   #false laziness w/ClientAPI/list_svcs
557
558   my @cust_svc = ();
559   #my @cust_pkg_usage = ();
560   #foreach my $cust_pkg ( $p->{'ncancelled'} 
561   #                       ? $cust_main->ncancelled_pkgs
562   #                       : $cust_main->unsuspended_pkgs ) {
563   foreach my $cust_pkg ( $cust_main->all_pkgs ) {
564     #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
565     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
566     #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
567   }
568
569   return {
570     'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
571   };
572
573 }
574
575 =item location_info
576
577 Returns location specific information for the customer. Takes a list of keys
578 and values as paramters with the following keys: custnum, secret
579
580 =cut
581
582 #I also monitor for changes to the additional locations that are applied to
583 # packages, and would like for those to be exportable as well.  basically the
584 # location data passed with the custnum.
585
586 sub location_info {
587   my( $class, %opt ) = @_;
588   return _shared_secret_error() unless _check_shared_secret($opt{secret});
589
590   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
591
592   my %return = (
593     'error'           => '',
594     'locations'       => [ map $_->hashref, @cust_location ],
595   );
596
597   return \%return;
598 }
599
600 =item change_package_location
601
602 Updates package location. Takes a list of keys and values 
603 as paramters with the following keys: 
604
605 pkgnum
606
607 secret
608
609 locationnum - pass this, or the following keys (don't pass both)
610
611 locationname
612
613 address1
614
615 address2
616
617 city
618
619 county
620
621 state
622
623 zip
624
625 addr_clean
626
627 country
628
629 censustract
630
631 censusyear
632
633 location_type
634
635 location_number
636
637 location_kind
638
639 incorporated
640
641 On error, returns a hashref with an 'error' key.
642 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
643 containing the new values.
644
645 =cut
646
647 sub change_package_location {
648   my $class = shift;
649   my %opt  = @_;
650   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
651
652   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
653     or return { 'error' => 'Unknown pkgnum' };
654
655   my %changeopt;
656
657   foreach my $field ( qw(
658     locationnum
659     locationname
660     address1
661     address2
662     city
663     county
664     state
665     zip
666     addr_clean
667     country
668     censustract
669     censusyear
670     location_type
671     location_number
672     location_kind
673     incorporated
674   )) {
675     $changeopt{$field} = $opt{$field} if $opt{$field};
676   }
677
678   $cust_pkg->API_change(%changeopt);
679 }
680
681 =item bill_now OPTION => VALUE, ...
682
683 Bills a single customer now, in the same fashion as the "Bill now" link in the
684 UI.
685
686 Returns a hash reference with a single key, 'error'.  If there is an error,   
687 the value contains the error, otherwise it is empty. Takes a list of keys and
688 values as parameters with the following keys:
689
690 =over 4
691
692 =item secret
693
694 API Secret (required)
695
696 =item custnum
697
698 Customer number (required)
699
700 =back
701
702 =cut
703
704 sub bill_now {
705   my( $class, %opt ) = @_;
706   return _shared_secret_error() unless _check_shared_secret($opt{secret});
707
708   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
709     or return { 'error' => 'Unknown custnum' };
710
711   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
712                                             'retry'      => 1,
713                                             'check_freq' =>'1d',
714                                           );
715
716    return { 'error' => $error,
717           };
718
719 }
720
721
722 #next.. Delete Advertising sources?
723
724 sub list_advertising_sources {
725   my( $class, %opt ) = @_;
726   return _shared_secret_error() unless _check_shared_secret($opt{secret});
727
728   my @sources = qsearch('part_referral', {}, '', "")
729     or return { 'error' => 'No referrals' };
730
731   my $return = {
732     'sources'       => [ map $_->hashref, @sources ],
733   };
734
735   $return;
736 }
737
738 sub add_advertising_source {
739   my( $class, %opt ) = @_;
740   return _shared_secret_error() unless _check_shared_secret($opt{secret});
741
742   use FS::part_referral;
743
744   my $new_source = $opt{source};
745
746   my $source = new FS::part_referral $new_source;
747
748   my $error = $source->insert;
749
750   my $return = {$source->hash};
751   $return = { 'error' => $error, } if $error;
752
753   $return;
754 }
755
756 sub edit_advertising_source {
757   my( $class, %opt ) = @_;
758   return _shared_secret_error() unless _check_shared_secret($opt{secret});
759
760   use FS::part_referral;
761
762   my $refnum = $opt{refnum};
763   my $source = $opt{source};
764
765   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
766   my $new = new FS::part_referral { $old->hash };
767
768   foreach my $key (keys %$source) {
769     $new->$key($source->{$key});
770   }
771
772   my $error = $new->replace;
773
774   my $return = {$new->hash};
775   $return = { 'error' => $error, } if $error;
776
777   $return;
778 }
779
780
781 ##
782 # helper subroutines
783 ##
784
785 sub _check_shared_secret {
786   shift eq FS::Conf->new->config('api_shared_secret');
787 }
788
789 sub _shared_secret_error {
790   return { 'error' => 'Incorrect shared secret' };
791 }
792
793 1;