047bb4e6046bab660ce0bf0caf74cc67033cee65
[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 order_package OPTION => VALUE, ...
654
655 Orders a new customer package.  Takes a list of keys and values as paramaters
656 with the following keys:
657
658 =over 4
659
660 =item secret
661
662 API Secret
663
664 =item custnum
665
666 =item pkgpart
667
668 =item quantity
669
670 =item start_date
671
672 =item contract_end
673
674 =item address1
675
676 =item address2
677
678 =item city
679
680 =item county
681
682 =item state
683
684 =item zip
685
686 =item country
687
688 =item setup_fee
689
690 Including this implements per-customer custom pricing for this package, overriding package definition pricing
691
692 =item recur_fee
693
694 Including this implements per-customer custom pricing for this package, overriding package definition pricing
695
696 =item invoice_details
697
698 A single string for just one detail line, or an array reference of one or more
699 lines of detail
700
701 =cut
702
703 sub order_package {
704   my( $class, %opt ) = @_;
705
706   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
707     or return { 'error' => 'Unknown custnum' };
708
709   #some conceptual false laziness w/cust_pkg/Import.pm
710
711   my $cust_pkg = new FS::cust_pkg {
712     'pkgpart'    => $opt{'pkgpart'},
713     'quantity'   => $opt{'quantity'} || 1,
714   };
715
716   #start_date and contract_end
717   foreach my $date_field (qw( start_date contract_end )) {
718     if ( $opt{$date_field} =~ /^(\d+)$/ ) {
719       $cust_pkg->$date_field( $opt{$date_field} );
720     } elsif ( $opt{$date_field} ) {
721       $cust_pkg->$date_field( str2time( $opt{$date_field} ) );
722     }
723   }
724
725   #especially this part for custom pkg price
726   # (false laziness w/cust_pkg/Import.pm)
727   my $s = $opt{'setup_fee'};
728   my $r = $opt{'recur_fee'};
729   my $part_pkg = $cust_pkg->part_pkg;
730   if (    ( length($s) && $s != $part_pkg->option('setup_fee') )
731        or ( length($r) && $r != $part_pkg->option('recur_fee') )
732      )
733   {
734     my $custom_part_pkg = $part_pkg->clone;
735     $custom_part_pkg->disabled('Y');
736     my %options = $part_pkg->options;
737     $options{'setup_fee'} = $s if length($s);
738     $options{'recur_fee'} = $r if length($r);
739     my $error = $custom_part_pkg->insert( options=>\%options );
740     return ( 'error' => "error customizing package: $error" ) if $error;
741     $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
742   }
743
744   my %order_pkg = ( 'cust_pkg' => $cust_pkg );
745
746   my @loc_fields = qw( address1 address2 city county state zip country );
747   if ( grep length($opt{$_}), @loc_fields ) {
748      $order_pkg{'cust_location'} = new FS::cust_location {
749        map { $_ => $opt{$_} } @loc_fields, 'custnum'
750      };
751   }
752
753   $order_pkg{'invoice_details'} = $opt{'invoice_details'}
754     if $opt{'invoice_details'};
755
756   my $error = $cust_main->order_pkg( %order_pkg );
757
758   #if ( $error ) {
759     return { 'error'  => $error,
760              #'pkgnum' => '',
761            };
762   #} else {
763   #  return { 'error'  => '',
764   #           #cust_main->order_pkg doesn't actually have a way to return pkgnum
765   #           #'pkgnum' => $pkgnum,
766   #         };
767   #}
768
769 }
770
771 =item change_package_location
772
773 Updates package location. Takes a list of keys and values 
774 as parameters with the following keys: 
775
776 pkgnum
777
778 secret
779
780 locationnum - pass this, or the following keys (don't pass both)
781
782 locationname
783
784 address1
785
786 address2
787
788 city
789
790 county
791
792 state
793
794 zip
795
796 addr_clean
797
798 country
799
800 censustract
801
802 censusyear
803
804 location_type
805
806 location_number
807
808 location_kind
809
810 incorporated
811
812 On error, returns a hashref with an 'error' key.
813 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
814 containing the new values.
815
816 =cut
817
818 sub change_package_location {
819   my $class = shift;
820   my %opt  = @_;
821   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
822
823   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
824     or return { 'error' => 'Unknown pkgnum' };
825
826   my %changeopt;
827
828   foreach my $field ( qw(
829     locationnum
830     locationname
831     address1
832     address2
833     city
834     county
835     state
836     zip
837     addr_clean
838     country
839     censustract
840     censusyear
841     location_type
842     location_number
843     location_kind
844     incorporated
845   )) {
846     $changeopt{$field} = $opt{$field} if $opt{$field};
847   }
848
849   $cust_pkg->API_change(%changeopt);
850 }
851
852 =item bill_now OPTION => VALUE, ...
853
854 Bills a single customer now, in the same fashion as the "Bill now" link in the
855 UI.
856
857 Returns a hash reference with a single key, 'error'.  If there is an error,   
858 the value contains the error, otherwise it is empty. Takes a list of keys and
859 values as parameters with the following keys:
860
861 =over 4
862
863 =item secret
864
865 API Secret (required)
866
867 =item custnum
868
869 Customer number (required)
870
871 =back
872
873 =cut
874
875 sub bill_now {
876   my( $class, %opt ) = @_;
877   return _shared_secret_error() unless _check_shared_secret($opt{secret});
878
879   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
880     or return { 'error' => 'Unknown custnum' };
881
882   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
883                                             'retry'      => 1,
884                                             'check_freq' =>'1d',
885                                           );
886
887    return { 'error' => $error,
888           };
889
890 }
891
892
893 #next.. Delete Advertising sources?
894
895 =item list_advertising_sources OPTION => VALUE, ...
896
897 Lists all advertising sources.
898
899 =over
900
901 =item secret
902
903 API Secret
904
905 =back
906
907 Example:
908
909   my $result = FS::API->list_advertising_sources(
910     'secret'  => 'sharingiscaring',
911   );
912
913   if ( $result->{'error'} ) {
914     die $result->{'error'};
915   } else {
916     # list advertising sources returns an array of hashes for sources.
917     print Dumper($result->{'sources'});
918   }
919
920 =cut
921
922 #list_advertising_sources
923 sub list_advertising_sources {
924   my( $class, %opt ) = @_;
925   return _shared_secret_error() unless _check_shared_secret($opt{secret});
926
927   my @sources = qsearch('part_referral', {}, '', "")
928     or return { 'error' => 'No referrals' };
929
930   my $return = {
931     'sources'       => [ map $_->hashref, @sources ],
932   };
933
934   $return;
935 }
936
937 =item add_advertising_source OPTION => VALUE, ...
938
939 Add a new advertising source.
940
941 =over
942
943 =item secret
944
945 API Secret
946
947 =item referral
948
949 Referral name
950
951 =item disabled
952
953 Referral disabled, Y for disabled or nothing for enabled
954
955 =item agentnum
956
957 Agent ID number
958
959 =item title
960
961 External referral ID
962
963 =back
964
965 Example:
966
967   my $result = FS::API->add_advertising_source(
968     'secret'     => 'sharingiscaring',
969     'referral'   => 'test referral',
970
971     #optional
972     'disabled'   => 'Y',
973     'agentnum'   => '2', #agent id number
974     'title'      => 'test title',
975   );
976
977   if ( $result->{'error'} ) {
978     die $result->{'error'};
979   } else {
980     # add_advertising_source returns new source upon success.
981     print Dumper($result);
982   }
983
984 =cut
985
986 #add_advertising_source
987 sub add_advertising_source {
988   my( $class, %opt ) = @_;
989   return _shared_secret_error() unless _check_shared_secret($opt{secret});
990
991   use FS::part_referral;
992
993   my $new_source = $opt{source};
994
995   my $source = new FS::part_referral $new_source;
996
997   my $error = $source->insert;
998
999   my $return = {$source->hash};
1000   $return = { 'error' => $error, } if $error;
1001
1002   $return;
1003 }
1004
1005 =item edit_advertising_source OPTION => VALUE, ...
1006
1007 Edit a advertising source.
1008
1009 =over
1010
1011 =item secret
1012
1013 API Secret
1014
1015 =item refnum
1016
1017 Referral number to edit
1018
1019 =item source
1020
1021 hash of edited source fields.
1022
1023 =over
1024
1025 =item referral
1026
1027 Referral name
1028
1029 =item disabled
1030
1031 Referral disabled, Y for disabled or nothing for enabled
1032
1033 =item agentnum
1034
1035 Agent ID number
1036
1037 =item title
1038
1039 External referral ID
1040
1041 =back
1042
1043 =back
1044
1045 Example:
1046
1047   my $result = FS::API->edit_advertising_source(
1048     'secret'     => 'sharingiscaring',
1049     'refnum'     => '4', # referral number to edit
1050     'source'     => {
1051        #optional
1052        'referral'   => 'test referral',
1053        'disabled'   => 'Y',
1054        'agentnum'   => '2', #agent id number
1055        'title'      => 'test title',
1056     }
1057   );
1058
1059   if ( $result->{'error'} ) {
1060     die $result->{'error'};
1061   } else {
1062     # edit_advertising_source returns updated source upon success.
1063     print Dumper($result);
1064   }
1065
1066 =cut
1067
1068 #edit_advertising_source
1069 sub edit_advertising_source {
1070   my( $class, %opt ) = @_;
1071   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1072
1073   use FS::part_referral;
1074
1075   my $refnum = $opt{refnum};
1076   my $source = $opt{source};
1077
1078   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1079   my $new = new FS::part_referral { $old->hash };
1080
1081   foreach my $key (keys %$source) {
1082     $new->$key($source->{$key});
1083   }
1084
1085   my $error = $new->replace;
1086
1087   my $return = {$new->hash};
1088   $return = { 'error' => $error, } if $error;
1089
1090   $return;
1091 }
1092
1093
1094 ##
1095 # helper subroutines
1096 ##
1097
1098 sub _check_shared_secret {
1099   shift eq FS::Conf->new->config('api_shared_secret');
1100 }
1101
1102 sub _shared_secret_error {
1103   return { 'error' => 'Incorrect shared secret' };
1104 }
1105
1106 1;