RT# 73422 Changes to report Customer Contacts
[freeside.git] / FS / FS / API.pm
1 package FS::API;
2
3 use strict;
4 use Date::Parse;
5 use FS::Conf;
6 use FS::Record qw( qsearch qsearchs );
7 use FS::cust_main;
8 use FS::cust_location;
9 use FS::cust_pay;
10 use FS::cust_credit;
11 use FS::cust_refund;
12 use FS::cust_pkg;
13
14 =head1 NAME
15
16 FS::API - Freeside backend API
17
18 =head1 SYNOPSIS
19
20   use Frontier::Client;
21   use Data::Dumper;
22
23   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
24                                               # the traffic
25
26   my $xmlrpc = new Frontier::Client url=>$url;
27
28   my $result = $xmlrpc->call( 'FS.API.customer_info',
29                                 'secret'  => 'sharingiscaring',
30                                 'custnum' => 181318,
31                             );
32
33   print Dumper($result);
34
35 =head1 DESCRIPTION
36
37 This module implements a backend API for advanced back-office integration.
38
39 In contrast to the self-service API, which authenticates an end-user and offers
40 functionality to that end user, the backend API performs a simple shared-secret
41 authentication and offers full, administrator functionality, enabling
42 integration with other back-office systems.  Only access this API from a secure 
43 network from other backoffice machines. DON'T use this API to create customer 
44 portal functionality.
45
46 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
47 the port by default, only allow access from back-office servers with the same
48 security precations as the Freeside server, and encrypt the communication
49 channel (for example, with an SSH tunnel or VPN) rather than accessing it
50 in plaintext.
51
52 =head1 METHODS
53
54 =over 4
55
56 =item insert_payment OPTION => VALUE, ...
57
58 Adds a new payment to a customers account. Takes a list of keys and values as
59 paramters with the following keys:
60
61 =over 4
62
63 =item secret
64
65 API Secret
66
67 =item custnum
68
69 Customer number
70
71 =item payby
72
73 Payment type
74
75 =item paid
76
77 Amount paid
78
79 =item _date
80
81 Option date for payment
82
83 =item order_number
84
85 Optional order number
86
87 =back
88
89 Example:
90
91   my $result = FS::API->insert_payment(
92     'secret'  => 'sharingiscaring',
93     'custnum' => 181318,
94     'payby'   => 'CASH',
95     'paid'    => '54.32',
96
97     #optional
98     '_date'   => 1397977200, #UNIX timestamp
99     'order_number' => '12345',
100   );
101
102   if ( $result->{'error'} ) {
103     die $result->{'error'};
104   } else {
105     #payment was inserted
106     print "paynum ". $result->{'paynum'};
107   }
108
109 =cut
110
111 #enter cash payment
112 sub insert_payment {
113   my($class, %opt) = @_;
114   return _shared_secret_error() unless _check_shared_secret($opt{secret});
115
116   #less "raw" than this?  we are the backoffice API, and aren't worried
117   # about version migration ala cust_main/cust_location here
118   my $cust_pay = new FS::cust_pay { %opt };
119   my $error = $cust_pay->insert( 'manual'=>1 );
120   return { 'error'  => $error,
121            'paynum' => $cust_pay->paynum,
122          };
123 }
124
125 # pass the phone number ( from svc_phone ) 
126 sub insert_payment_phonenum {
127   my($class, %opt) = @_;
128   $class->_by_phonenum('insert_payment', %opt);
129 }
130
131 sub _by_phonenum {
132   my($class, $method, %opt) = @_;
133   return _shared_secret_error() unless _check_shared_secret($opt{secret});
134
135   my $phonenum = delete $opt{'phonenum'};
136
137   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
138     or return { 'error' => 'Unknown phonenum' };
139
140   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
141     or return { 'error' => 'Unlinked phonenum' };
142
143   $opt{'custnum'} = $cust_pkg->custnum;
144
145   $class->$method(%opt);
146 }
147
148 =item insert_credit OPTION => VALUE, ...
149
150 Adds a a credit to a customers account.  Takes a list of keys and values as
151 parameters with the following keys
152
153 =over 
154
155 =item secret
156
157 API Secret
158
159 =item custnum
160
161 customer number
162
163 =item amount
164
165 Amount of the credit
166
167 =item _date
168
169 The date the credit will be posted
170
171 =back
172
173 Example:
174
175   my $result = FS::API->insert_credit(
176     'secret'  => 'sharingiscaring',
177     'custnum' => 181318,
178     'amount'  => '54.32',
179
180     #optional
181     '_date'   => 1397977200, #UNIX timestamp
182   );
183
184   if ( $result->{'error'} ) {
185     die $result->{'error'};
186   } else {
187     #credit was inserted
188     print "crednum ". $result->{'crednum'};
189   }
190
191 =cut
192
193 #Enter credit
194 sub insert_credit {
195   my($class, %opt) = @_;
196   return _shared_secret_error() unless _check_shared_secret($opt{secret});
197
198   $opt{'reasonnum'} ||= FS::Conf->new->config('api_credit_reason');
199
200   #less "raw" than this?  we are the backoffice API, and aren't worried
201   # about version migration ala cust_main/cust_location here
202   my $cust_credit = new FS::cust_credit { %opt };
203   my $error = $cust_credit->insert;
204   return { 'error'  => $error,
205            'crednum' => $cust_credit->crednum,
206          };
207 }
208
209 # pass the phone number ( from svc_phone ) 
210 sub insert_credit_phonenum {
211   my($class, %opt) = @_;
212   $class->_by_phonenum('insert_credit', %opt);
213 }
214
215 =item apply_payments_and_credits
216
217 Applies payments and credits for this customer.  Takes a list of keys and
218 values as parameter with the following keys:
219
220 =over 4
221
222 =item secret
223
224 API secret
225
226 =item custnum
227
228 Customer number
229
230 =back
231
232 =cut
233
234 #apply payments and credits
235 sub apply_payments_and_credits {
236   my($class, %opt) = @_;
237   return _shared_secret_error() unless _check_shared_secret($opt{secret});
238
239   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
240     or return { 'error' => 'Unknown custnum' };
241
242   my $error = $cust_main->apply_payments_and_credits( 'manual'=>1 );
243   return { 'error'  => $error, };
244 }
245
246 =item insert_refund OPTION => VALUE, ...
247
248 Adds a a credit to a customers account.  Takes a list of keys and values as
249 parmeters with the following keys: custnum, payby, refund
250
251 Example:
252
253   my $result = FS::API->insert_refund(
254     'secret'  => 'sharingiscaring',
255     'custnum' => 181318,
256     'payby'   => 'CASH',
257     'refund'  => '54.32',
258
259     #optional
260     '_date'   => 1397977200, #UNIX timestamp
261   );
262
263   if ( $result->{'error'} ) {
264     die $result->{'error'};
265   } else {
266     #refund was inserted
267     print "refundnum ". $result->{'crednum'};
268   }
269
270 =cut
271
272 #Enter cash refund.
273 sub insert_refund {
274   my($class, %opt) = @_;
275   return _shared_secret_error() unless _check_shared_secret($opt{secret});
276
277   # when github pull request #24 is merged,
278   #  will have to change over to default reasonnum like credit
279   # but until then, this will do
280   $opt{'reason'} ||= 'API refund';
281
282   #less "raw" than this?  we are the backoffice API, and aren't worried
283   # about version migration ala cust_main/cust_location here
284   my $cust_refund = new FS::cust_refund { %opt };
285   my $error = $cust_refund->insert;
286   return { 'error'     => $error,
287            'refundnum' => $cust_refund->refundnum,
288          };
289 }
290
291 # pass the phone number ( from svc_phone ) 
292 sub insert_refund_phonenum {
293   my($class, %opt) = @_;
294   $class->_by_phonenum('insert_refund', %opt);
295 }
296
297 #---
298
299 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
300 # figure out how to trigger something when those things change
301
302 # long-term: package changes?
303
304 =item new_customer OPTION => VALUE, ...
305
306 Creates a new customer. Takes a list of keys and values as parameters with the
307 following keys:
308
309 =over 4
310
311 =item secret
312
313 API Secret
314
315 =item first
316
317 first name (required)
318
319 =item last
320
321 last name (required)
322
323 =item ss
324
325 (not typically collected; mostly used for ACH transactions)
326
327 =item company
328
329 Company name
330
331 =item address1 (required)
332
333 Address line one
334
335 =item city (required)
336
337 City
338
339 =item county
340
341 County
342
343 =item state (required)
344
345 State
346
347 =item zip (required)
348
349 Zip or postal code
350
351 =item country
352
353 2 Digit Country Code
354
355 =item latitude
356
357 latitude
358
359 =item Longitude
360
361 longitude
362
363 =item geocode
364
365 Currently used for third party tax vendor lookups
366
367 =item censustract
368
369 Used for determining FCC 477 reporting
370
371 =item censusyear
372
373 Used for determining FCC 477 reporting
374
375 =item daytime
376
377 Daytime phone number
378
379 =item night
380
381 Evening phone number
382
383 =item fax
384
385 Fax number
386
387 =item mobile
388
389 Mobile number
390
391 =item invoicing_list
392
393 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),
394 postal_invoicing
395 Set to 1 to enable postal invoicing
396
397 =item referral_custnum
398
399 Referring customer number
400
401 =item salesnum
402
403 Sales person number
404
405 =item agentnum
406
407 Agent number
408
409 =item agent_custid
410
411 Agent specific customer number
412
413 =item referral_custnum
414
415 Referring customer number
416
417 =back
418
419 =cut
420
421 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
422 # but approaching this from a clean start / back-office perspective
423 #  i.e. no package/service, no immediate credit card run, etc.
424
425 sub new_customer {
426   my( $class, %opt ) = @_;
427   return _shared_secret_error() unless _check_shared_secret($opt{secret});
428
429   #default agentnum like signup_server-default_agentnum?
430   #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
431  
432   #same for refnum like signup_server-default_refnum
433   $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
434
435   $class->API_insert( %opt );
436 }
437
438 =item update_customer
439
440 Updates an existing customer. Passing an empty value clears that field, while
441 NOT passing that key/value at all leaves it alone. Takes a list of keys and
442 values as parameters with the following keys:
443
444 =over 4
445
446 =item secret
447
448 API Secret (required)
449
450 =item custnum
451
452 Customer number (required)
453
454 =item first
455
456 first name 
457
458 =item last
459
460 last name 
461
462 =item company
463
464 Company name
465
466 =item address1 
467
468 Address line one
469
470 =item city 
471
472 City
473
474 =item county
475
476 County
477
478 =item state 
479
480 State
481
482 =item zip 
483
484 Zip or postal code
485
486 =item country
487
488 2 Digit Country Code
489
490 =item daytime
491
492 Daytime phone number
493
494 =item night
495
496 Evening phone number
497
498 =item fax
499
500 Fax number
501
502 =item mobile
503
504 Mobile number
505
506 =item invoicing_list
507
508 Comma-separated list of email addresses for email invoices. The special value 
509 'POST' is used to designate postal invoicing (it may be specified alone or in
510 addition to email addresses),
511 postal_invoicing
512 Set to 1 to enable postal invoicing
513
514 =item referral_custnum
515
516 Referring customer number
517
518 =item salesnum
519
520 Sales person number
521
522 =item agentnum
523
524 Agent number
525
526 =back
527
528 =cut
529
530 sub update_customer {
531   my( $class, %opt ) = @_;
532   return _shared_secret_error() unless _check_shared_secret($opt{secret});
533
534   FS::cust_main->API_update( %opt );
535 }
536
537 =item customer_info OPTION => VALUE, ...
538
539 Returns general customer information. Takes a list of keys and values as
540 parameters with the following keys: custnum, secret 
541
542 Example:
543
544   use Frontier::Client;
545   use Data::Dumper;
546
547   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
548                                               # the traffic
549
550   my $xmlrpc = new Frontier::Client url=>$url;
551
552   my $result = $xmlrpc->call( 'FS.API.customer_info',
553                                 'secret'  => 'sharingiscaring',
554                                 'custnum' => 181318,
555                             );
556
557   print Dumper($result);
558
559 =cut
560
561 sub customer_info {
562   my( $class, %opt ) = @_;
563   return _shared_secret_error() unless _check_shared_secret($opt{secret});
564
565   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
566     or return { 'error' => 'Unknown custnum' };
567
568   $cust_main->API_getinfo;
569 }
570
571 =item customer_list_svcs OPTION => VALUE, ...
572
573 Returns customer service information.  Takes a list of keys and values as
574 parameters with the following keys: custnum, secret
575
576 Example:
577
578   use Frontier::Client;
579   use Data::Dumper;
580
581   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
582                                               # the traffic
583
584   my $xmlrpc = new Frontier::Client url=>$url;
585
586   my $result = $xmlrpc->call( 'FS.API.customer_list_svcs',
587                                 'secret'  => 'sharingiscaring',
588                                 'custnum' => 181318,
589                             );
590
591   print Dumper($result);
592
593   foreach my $cust_svc ( @{ $result->{'cust_svc'} } ) {
594     #print $cust_svc->{mac_addr}."\n" if exists $cust_svc->{mac_addr};
595     print $cust_svc->{circuit_id}."\n" if exists $cust_svc->{circuit_id};
596   }
597
598 =cut
599
600 sub customer_list_svcs {
601   my( $class, %opt ) = @_;
602   return _shared_secret_error() unless _check_shared_secret($opt{secret});
603
604   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
605     or return { 'error' => 'Unknown custnum' };
606
607   #$cust_main->API_list_svcs;
608
609   #false laziness w/ClientAPI/list_svcs
610
611   my @cust_svc = ();
612   #my @cust_pkg_usage = ();
613   #foreach my $cust_pkg ( $p->{'ncancelled'} 
614   #                       ? $cust_main->ncancelled_pkgs
615   #                       : $cust_main->unsuspended_pkgs ) {
616   foreach my $cust_pkg ( $cust_main->all_pkgs ) {
617     #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
618     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
619     #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
620   }
621
622   return {
623     'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
624   };
625
626 }
627
628 =item location_info
629
630 Returns location specific information for the customer. Takes a list of keys
631 and values as paramters with the following keys: custnum, secret
632
633 =cut
634
635 #I also monitor for changes to the additional locations that are applied to
636 # packages, and would like for those to be exportable as well.  basically the
637 # location data passed with the custnum.
638
639 sub location_info {
640   my( $class, %opt ) = @_;
641   return _shared_secret_error() unless _check_shared_secret($opt{secret});
642
643   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
644
645   my %return = (
646     'error'           => '',
647     'locations'       => [ map $_->hashref, @cust_location ],
648   );
649
650   return \%return;
651 }
652
653 =item list_customer_packages OPTION => VALUE, ...
654
655 Lists all customer packages.
656
657 =over
658
659 =item secret
660
661 API Secret
662
663 =item custnum
664
665 Customer Number
666
667 =back
668
669 Example:
670
671   my $result = FS::API->list_packages(
672     'secret'  => 'sharingiscaring',
673     'custnum'  => custnum,
674   );
675
676   if ( $result->{'error'} ) {
677     die $result->{'error'};
678   } else {
679     # list packages returns an array of hashes for packages ordered by custnum and pkgnum.
680     print Dumper($result->{'pkgs'});
681   }
682
683 =cut
684
685 sub list_customer_packages {
686   my( $class, %opt ) = @_;
687   return _shared_secret_error() unless _check_shared_secret($opt{secret});
688
689   my $sql_query = FS::cust_pkg->search({ 'custnum' => $opt{custnum}, });
690
691   $sql_query->{order_by} = 'ORDER BY custnum, pkgnum';
692
693   my @packages = qsearch($sql_query)
694     or return { 'error' => 'No packages' };
695
696   my $return = {
697     'packages'       => [ map $_->hashref, @packages ],
698   };
699
700   $return;
701 }
702
703 =item package_status OPTION => VALUE, ...
704
705 Get package status.
706
707 =over
708
709 =item secret
710
711 API Secret
712
713 =item pkgnum
714
715 Package Number
716
717 =back
718
719 Example:
720
721   my $result = FS::API->package_status(
722     'secret'  => 'sharingiscaring',
723     'pkgnum'  => pkgnum,
724   );
725
726   if ( $result->{'error'} ) {
727     die $result->{'error'};
728   } else {
729     # package status returns a hash with the status for a package.
730     print Dumper($result->{'status'});
731   }
732
733 =cut
734
735 sub package_status {
736   my( $class, %opt ) = @_;
737   return _shared_secret_error() unless _check_shared_secret($opt{secret});
738
739   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{pkgnum} } )
740     or return { 'error' => 'No packages' };
741
742   my $return = {
743     'status' => $cust_pkg->status,
744   };
745
746   $return;
747 }
748
749 =item order_package OPTION => VALUE, ...
750
751 Orders a new customer package.  Takes a list of keys and values as paramaters
752 with the following keys:
753
754 =over 4
755
756 =item secret
757
758 API Secret
759
760 =item custnum
761
762 =item pkgpart
763
764 =item quantity
765
766 =item start_date
767
768 =item contract_end
769
770 =item address1
771
772 =item address2
773
774 =item city
775
776 =item county
777
778 =item state
779
780 =item zip
781
782 =item country
783
784 =item setup_fee
785
786 Including this implements per-customer custom pricing for this package, overriding package definition pricing
787
788 =item recur_fee
789
790 Including this implements per-customer custom pricing for this package, overriding package definition pricing
791
792 =item invoice_details
793
794 A single string for just one detail line, or an array reference of one or more
795 lines of detail
796
797 =cut
798
799 sub order_package {
800   my( $class, %opt ) = @_;
801
802   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
803     or return { 'error' => 'Unknown custnum' };
804
805   #some conceptual false laziness w/cust_pkg/Import.pm
806
807   my $cust_pkg = new FS::cust_pkg {
808     'pkgpart'    => $opt{'pkgpart'},
809     'quantity'   => $opt{'quantity'} || 1,
810   };
811
812   #start_date and contract_end
813   foreach my $date_field (qw( start_date contract_end )) {
814     if ( $opt{$date_field} =~ /^(\d+)$/ ) {
815       $cust_pkg->$date_field( $opt{$date_field} );
816     } elsif ( $opt{$date_field} ) {
817       $cust_pkg->$date_field( str2time( $opt{$date_field} ) );
818     }
819   }
820
821   #especially this part for custom pkg price
822   # (false laziness w/cust_pkg/Import.pm)
823   my $s = $opt{'setup_fee'};
824   my $r = $opt{'recur_fee'};
825   my $part_pkg = $cust_pkg->part_pkg;
826   if (    ( length($s) && $s != $part_pkg->option('setup_fee') )
827        or ( length($r) && $r != $part_pkg->option('recur_fee') )
828      )
829   {
830
831     local($FS::part_pkg::skip_pkg_svc_hack) = 1;
832
833     my $custom_part_pkg = $part_pkg->clone;
834     $custom_part_pkg->disabled('Y');
835     my %options = $part_pkg->options;
836     $options{'setup_fee'} = $s if length($s);
837     $options{'recur_fee'} = $r if length($r);
838     my $error = $custom_part_pkg->insert( options=>\%options );
839     return ( 'error' => "error customizing package: $error" ) if $error;
840
841     #not ->pkg_svc, we want to ignore links and clone the actual package def
842     foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
843       my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
844       $c_pkg_svc->pkgsvcnum('');
845       $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
846       my $error = $c_pkg_svc->insert;
847       return "error customizing package: $error" if $error;
848     }
849
850     $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
851
852   }
853
854   my %order_pkg = ( 'cust_pkg' => $cust_pkg );
855
856   my @loc_fields = qw( address1 address2 city county state zip country );
857   if ( grep length($opt{$_}), @loc_fields ) {
858      $order_pkg{'cust_location'} = new FS::cust_location {
859        map { $_ => $opt{$_} } @loc_fields, 'custnum'
860      };
861   }
862
863   $order_pkg{'invoice_details'} = $opt{'invoice_details'}
864     if $opt{'invoice_details'};
865
866   my $error = $cust_main->order_pkg( %order_pkg );
867
868   #if ( $error ) {
869     return { 'error'  => $error,
870              #'pkgnum' => '',
871            };
872   #} else {
873   #  return { 'error'  => '',
874   #           #cust_main->order_pkg doesn't actually have a way to return pkgnum
875   #           #'pkgnum' => $pkgnum,
876   #         };
877   #}
878
879 }
880
881 =item change_package_location
882
883 Updates package location. Takes a list of keys and values 
884 as parameters with the following keys: 
885
886 pkgnum
887
888 secret
889
890 locationnum - pass this, or the following keys (don't pass both)
891
892 locationname
893
894 address1
895
896 address2
897
898 city
899
900 county
901
902 state
903
904 zip
905
906 addr_clean
907
908 country
909
910 censustract
911
912 censusyear
913
914 location_type
915
916 location_number
917
918 location_kind
919
920 incorporated
921
922 On error, returns a hashref with an 'error' key.
923 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
924 containing the new values.
925
926 =cut
927
928 sub change_package_location {
929   my $class = shift;
930   my %opt  = @_;
931   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
932
933   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
934     or return { 'error' => 'Unknown pkgnum' };
935
936   my %changeopt;
937
938   foreach my $field ( qw(
939     locationnum
940     locationname
941     address1
942     address2
943     city
944     county
945     state
946     zip
947     addr_clean
948     country
949     censustract
950     censusyear
951     location_type
952     location_number
953     location_kind
954     incorporated
955   )) {
956     $changeopt{$field} = $opt{$field} if $opt{$field};
957   }
958
959   $cust_pkg->API_change(%changeopt);
960 }
961
962 =item bill_now OPTION => VALUE, ...
963
964 Bills a single customer now, in the same fashion as the "Bill now" link in the
965 UI.
966
967 Returns a hash reference with a single key, 'error'.  If there is an error,   
968 the value contains the error, otherwise it is empty. Takes a list of keys and
969 values as parameters with the following keys:
970
971 =over 4
972
973 =item secret
974
975 API Secret (required)
976
977 =item custnum
978
979 Customer number (required)
980
981 =back
982
983 =cut
984
985 sub bill_now {
986   my( $class, %opt ) = @_;
987   return _shared_secret_error() unless _check_shared_secret($opt{secret});
988
989   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
990     or return { 'error' => 'Unknown custnum' };
991
992   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
993                                             'retry'      => 1,
994                                             'check_freq' =>'1d',
995                                           );
996
997    return { 'error' => $error,
998           };
999
1000 }
1001
1002
1003 #next.. Delete Advertising sources?
1004
1005 =item list_advertising_sources OPTION => VALUE, ...
1006
1007 Lists all advertising sources.
1008
1009 =over
1010
1011 =item secret
1012
1013 API Secret
1014
1015 =back
1016
1017 Example:
1018
1019   my $result = FS::API->list_advertising_sources(
1020     'secret'  => 'sharingiscaring',
1021   );
1022
1023   if ( $result->{'error'} ) {
1024     die $result->{'error'};
1025   } else {
1026     # list advertising sources returns an array of hashes for sources.
1027     print Dumper($result->{'sources'});
1028   }
1029
1030 =cut
1031
1032 #list_advertising_sources
1033 sub list_advertising_sources {
1034   my( $class, %opt ) = @_;
1035   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1036
1037   my @sources = qsearch('part_referral', {}, '', "")
1038     or return { 'error' => 'No referrals' };
1039
1040   my $return = {
1041     'sources'       => [ map $_->hashref, @sources ],
1042   };
1043
1044   $return;
1045 }
1046
1047 =item add_advertising_source OPTION => VALUE, ...
1048
1049 Add a new advertising source.
1050
1051 =over
1052
1053 =item secret
1054
1055 API Secret
1056
1057 =item referral
1058
1059 Referral name
1060
1061 =item disabled
1062
1063 Referral disabled, Y for disabled or nothing for enabled
1064
1065 =item agentnum
1066
1067 Agent ID number
1068
1069 =item title
1070
1071 External referral ID
1072
1073 =back
1074
1075 Example:
1076
1077   my $result = FS::API->add_advertising_source(
1078     'secret'     => 'sharingiscaring',
1079     'referral'   => 'test referral',
1080
1081     #optional
1082     'disabled'   => 'Y',
1083     'agentnum'   => '2', #agent id number
1084     'title'      => 'test title',
1085   );
1086
1087   if ( $result->{'error'} ) {
1088     die $result->{'error'};
1089   } else {
1090     # add_advertising_source returns new source upon success.
1091     print Dumper($result);
1092   }
1093
1094 =cut
1095
1096 #add_advertising_source
1097 sub add_advertising_source {
1098   my( $class, %opt ) = @_;
1099   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1100
1101   use FS::part_referral;
1102
1103   my $new_source = $opt{source};
1104
1105   my $source = new FS::part_referral $new_source;
1106
1107   my $error = $source->insert;
1108
1109   my $return = {$source->hash};
1110   $return = { 'error' => $error, } if $error;
1111
1112   $return;
1113 }
1114
1115 =item edit_advertising_source OPTION => VALUE, ...
1116
1117 Edit a advertising source.
1118
1119 =over
1120
1121 =item secret
1122
1123 API Secret
1124
1125 =item refnum
1126
1127 Referral number to edit
1128
1129 =item source
1130
1131 hash of edited source fields.
1132
1133 =over
1134
1135 =item referral
1136
1137 Referral name
1138
1139 =item disabled
1140
1141 Referral disabled, Y for disabled or nothing for enabled
1142
1143 =item agentnum
1144
1145 Agent ID number
1146
1147 =item title
1148
1149 External referral ID
1150
1151 =back
1152
1153 =back
1154
1155 Example:
1156
1157   my $result = FS::API->edit_advertising_source(
1158     'secret'     => 'sharingiscaring',
1159     'refnum'     => '4', # referral number to edit
1160     'source'     => {
1161        #optional
1162        'referral'   => 'test referral',
1163        'disabled'   => 'Y',
1164        'agentnum'   => '2', #agent id number
1165        'title'      => 'test title',
1166     }
1167   );
1168
1169   if ( $result->{'error'} ) {
1170     die $result->{'error'};
1171   } else {
1172     # edit_advertising_source returns updated source upon success.
1173     print Dumper($result);
1174   }
1175
1176 =cut
1177
1178 #edit_advertising_source
1179 sub edit_advertising_source {
1180   my( $class, %opt ) = @_;
1181   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1182
1183   use FS::part_referral;
1184
1185   my $refnum = $opt{refnum};
1186   my $source = $opt{source};
1187
1188   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1189   my $new = new FS::part_referral { $old->hash };
1190
1191   foreach my $key (keys %$source) {
1192     $new->$key($source->{$key});
1193   }
1194
1195   my $error = $new->replace;
1196
1197   my $return = {$new->hash};
1198   $return = { 'error' => $error, } if $error;
1199
1200   $return;
1201 }
1202
1203
1204 ##
1205 # helper subroutines
1206 ##
1207
1208 sub _check_shared_secret {
1209   shift eq FS::Conf->new->config('api_shared_secret');
1210 }
1211
1212 sub _shared_secret_error {
1213   return { 'error' => 'Incorrect shared secret' };
1214 }
1215
1216 1;