fix custom pricing vs. services issue in API order_package call too, RT#77484
[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
735     local($FS::part_pkg::skip_pkg_svc_hack) = 1;
736
737     my $custom_part_pkg = $part_pkg->clone;
738     $custom_part_pkg->disabled('Y');
739     my %options = $part_pkg->options;
740     $options{'setup_fee'} = $s if length($s);
741     $options{'recur_fee'} = $r if length($r);
742     my $error = $custom_part_pkg->insert( options=>\%options );
743     return ( 'error' => "error customizing package: $error" ) if $error;
744
745     #not ->pkg_svc, we want to ignore links and clone the actual package def
746     foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
747       my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
748       $c_pkg_svc->pkgsvcnum('');
749       $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
750       my $error = $c_pkg_svc->insert;
751       return "error customizing package: $error" if $error;
752     }
753
754     $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
755
756   }
757
758   my %order_pkg = ( 'cust_pkg' => $cust_pkg );
759
760   my @loc_fields = qw( address1 address2 city county state zip country );
761   if ( grep length($opt{$_}), @loc_fields ) {
762      $order_pkg{'cust_location'} = new FS::cust_location {
763        map { $_ => $opt{$_} } @loc_fields, 'custnum'
764      };
765   }
766
767   $order_pkg{'invoice_details'} = $opt{'invoice_details'}
768     if $opt{'invoice_details'};
769
770   my $error = $cust_main->order_pkg( %order_pkg );
771
772   #if ( $error ) {
773     return { 'error'  => $error,
774              #'pkgnum' => '',
775            };
776   #} else {
777   #  return { 'error'  => '',
778   #           #cust_main->order_pkg doesn't actually have a way to return pkgnum
779   #           #'pkgnum' => $pkgnum,
780   #         };
781   #}
782
783 }
784
785 =item change_package_location
786
787 Updates package location. Takes a list of keys and values 
788 as parameters with the following keys: 
789
790 pkgnum
791
792 secret
793
794 locationnum - pass this, or the following keys (don't pass both)
795
796 locationname
797
798 address1
799
800 address2
801
802 city
803
804 county
805
806 state
807
808 zip
809
810 addr_clean
811
812 country
813
814 censustract
815
816 censusyear
817
818 location_type
819
820 location_number
821
822 location_kind
823
824 incorporated
825
826 On error, returns a hashref with an 'error' key.
827 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
828 containing the new values.
829
830 =cut
831
832 sub change_package_location {
833   my $class = shift;
834   my %opt  = @_;
835   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
836
837   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
838     or return { 'error' => 'Unknown pkgnum' };
839
840   my %changeopt;
841
842   foreach my $field ( qw(
843     locationnum
844     locationname
845     address1
846     address2
847     city
848     county
849     state
850     zip
851     addr_clean
852     country
853     censustract
854     censusyear
855     location_type
856     location_number
857     location_kind
858     incorporated
859   )) {
860     $changeopt{$field} = $opt{$field} if $opt{$field};
861   }
862
863   $cust_pkg->API_change(%changeopt);
864 }
865
866 =item bill_now OPTION => VALUE, ...
867
868 Bills a single customer now, in the same fashion as the "Bill now" link in the
869 UI.
870
871 Returns a hash reference with a single key, 'error'.  If there is an error,   
872 the value contains the error, otherwise it is empty. Takes a list of keys and
873 values as parameters with the following keys:
874
875 =over 4
876
877 =item secret
878
879 API Secret (required)
880
881 =item custnum
882
883 Customer number (required)
884
885 =back
886
887 =cut
888
889 sub bill_now {
890   my( $class, %opt ) = @_;
891   return _shared_secret_error() unless _check_shared_secret($opt{secret});
892
893   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
894     or return { 'error' => 'Unknown custnum' };
895
896   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
897                                             'retry'      => 1,
898                                             'check_freq' =>'1d',
899                                           );
900
901    return { 'error' => $error,
902           };
903
904 }
905
906
907 #next.. Delete Advertising sources?
908
909 =item list_advertising_sources OPTION => VALUE, ...
910
911 Lists all advertising sources.
912
913 =over
914
915 =item secret
916
917 API Secret
918
919 =back
920
921 Example:
922
923   my $result = FS::API->list_advertising_sources(
924     'secret'  => 'sharingiscaring',
925   );
926
927   if ( $result->{'error'} ) {
928     die $result->{'error'};
929   } else {
930     # list advertising sources returns an array of hashes for sources.
931     print Dumper($result->{'sources'});
932   }
933
934 =cut
935
936 #list_advertising_sources
937 sub list_advertising_sources {
938   my( $class, %opt ) = @_;
939   return _shared_secret_error() unless _check_shared_secret($opt{secret});
940
941   my @sources = qsearch('part_referral', {}, '', "")
942     or return { 'error' => 'No referrals' };
943
944   my $return = {
945     'sources'       => [ map $_->hashref, @sources ],
946   };
947
948   $return;
949 }
950
951 =item add_advertising_source OPTION => VALUE, ...
952
953 Add a new advertising source.
954
955 =over
956
957 =item secret
958
959 API Secret
960
961 =item referral
962
963 Referral name
964
965 =item disabled
966
967 Referral disabled, Y for disabled or nothing for enabled
968
969 =item agentnum
970
971 Agent ID number
972
973 =item title
974
975 External referral ID
976
977 =back
978
979 Example:
980
981   my $result = FS::API->add_advertising_source(
982     'secret'     => 'sharingiscaring',
983     'referral'   => 'test referral',
984
985     #optional
986     'disabled'   => 'Y',
987     'agentnum'   => '2', #agent id number
988     'title'      => 'test title',
989   );
990
991   if ( $result->{'error'} ) {
992     die $result->{'error'};
993   } else {
994     # add_advertising_source returns new source upon success.
995     print Dumper($result);
996   }
997
998 =cut
999
1000 #add_advertising_source
1001 sub add_advertising_source {
1002   my( $class, %opt ) = @_;
1003   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1004
1005   use FS::part_referral;
1006
1007   my $new_source = $opt{source};
1008
1009   my $source = new FS::part_referral $new_source;
1010
1011   my $error = $source->insert;
1012
1013   my $return = {$source->hash};
1014   $return = { 'error' => $error, } if $error;
1015
1016   $return;
1017 }
1018
1019 =item edit_advertising_source OPTION => VALUE, ...
1020
1021 Edit a advertising source.
1022
1023 =over
1024
1025 =item secret
1026
1027 API Secret
1028
1029 =item refnum
1030
1031 Referral number to edit
1032
1033 =item source
1034
1035 hash of edited source fields.
1036
1037 =over
1038
1039 =item referral
1040
1041 Referral name
1042
1043 =item disabled
1044
1045 Referral disabled, Y for disabled or nothing for enabled
1046
1047 =item agentnum
1048
1049 Agent ID number
1050
1051 =item title
1052
1053 External referral ID
1054
1055 =back
1056
1057 =back
1058
1059 Example:
1060
1061   my $result = FS::API->edit_advertising_source(
1062     'secret'     => 'sharingiscaring',
1063     'refnum'     => '4', # referral number to edit
1064     'source'     => {
1065        #optional
1066        'referral'   => 'test referral',
1067        'disabled'   => 'Y',
1068        'agentnum'   => '2', #agent id number
1069        'title'      => 'test title',
1070     }
1071   );
1072
1073   if ( $result->{'error'} ) {
1074     die $result->{'error'};
1075   } else {
1076     # edit_advertising_source returns updated source upon success.
1077     print Dumper($result);
1078   }
1079
1080 =cut
1081
1082 #edit_advertising_source
1083 sub edit_advertising_source {
1084   my( $class, %opt ) = @_;
1085   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1086
1087   use FS::part_referral;
1088
1089   my $refnum = $opt{refnum};
1090   my $source = $opt{source};
1091
1092   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1093   my $new = new FS::part_referral { $old->hash };
1094
1095   foreach my $key (keys %$source) {
1096     $new->$key($source->{$key});
1097   }
1098
1099   my $error = $new->replace;
1100
1101   my $return = {$new->hash};
1102   $return = { 'error' => $error, } if $error;
1103
1104   $return;
1105 }
1106
1107
1108 ##
1109 # helper subroutines
1110 ##
1111
1112 sub _check_shared_secret {
1113   shift eq FS::Conf->new->config('api_shared_secret');
1114 }
1115
1116 sub _shared_secret_error {
1117   return { 'error' => 'Incorrect shared secret' };
1118 }
1119
1120 1;