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