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