doc: return fields for customer_info, RT#84796
[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 ship_address1
376
377 =item ship_address2
378
379 =item ship_city
380
381 =item ship_county
382
383 =item ship_state
384
385 =item ship_zip
386
387 =item ship_country
388
389 Optional shipping address fields.  If sending an optional shipping address,
390 ship_address1, ship_city, ship_state and ship_zip are required.
391
392 =item daytime
393
394 Daytime phone number
395
396 =item night
397
398 Evening phone number
399
400 =item fax
401
402 Fax number
403
404 =item mobile
405
406 Mobile number
407
408 =item invoicing_list
409
410 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),
411 postal_invoicing
412 Set to 1 to enable postal invoicing
413
414 =item referral_custnum
415
416 Referring customer number
417
418 =item salesnum
419
420 Sales person number
421
422 =item agentnum
423
424 Agent number
425
426 =item agent_custid
427
428 Agent specific customer number
429
430 =item referral_custnum
431
432 Referring customer number
433
434 =back
435
436 =cut
437
438 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
439 # but approaching this from a clean start / back-office perspective
440 #  i.e. no package/service, no immediate credit card run, etc.
441
442 sub new_customer {
443   my( $class, %opt ) = @_;
444   return _shared_secret_error() unless _check_shared_secret($opt{secret});
445
446   #default agentnum like signup_server-default_agentnum?
447   #$opt{agentnum} ||= $conf->config('signup_server-default_agentnum');
448  
449   #same for refnum like signup_server-default_refnum
450   $opt{refnum} ||= FS::Conf->new->config('signup_server-default_refnum');
451
452   FS::cust_main->API_insert( %opt );
453 }
454
455 =item update_customer
456
457 Updates an existing customer. Passing an empty value clears that field, while
458 NOT passing that key/value at all leaves it alone. Takes a list of keys and
459 values as parameters with the following keys:
460
461 =over 4
462
463 =item secret
464
465 API Secret (required)
466
467 =item custnum
468
469 Customer number (required)
470
471 =item first
472
473 first name 
474
475 =item last
476
477 last name 
478
479 =item company
480
481 Company name
482
483 =item address1 
484
485 Address line one
486
487 =item city 
488
489 City
490
491 =item county
492
493 County
494
495 =item state 
496
497 State
498
499 =item zip 
500
501 Zip or postal code
502
503 =item country
504
505 2 Digit Country Code
506
507 =item daytime
508
509 Daytime phone number
510
511 =item night
512
513 Evening phone number
514
515 =item fax
516
517 Fax number
518
519 =item mobile
520
521 Mobile number
522
523 =item invoicing_list
524
525 Comma-separated list of email addresses for email invoices. The special value 
526 'POST' is used to designate postal invoicing (it may be specified alone or in
527 addition to email addresses),
528 postal_invoicing
529 Set to 1 to enable postal invoicing
530
531 =item referral_custnum
532
533 Referring customer number
534
535 =item salesnum
536
537 Sales person number
538
539 =item agentnum
540
541 Agent number
542
543 =back
544
545 =cut
546
547 sub update_customer {
548   my( $class, %opt ) = @_;
549   return _shared_secret_error() unless _check_shared_secret($opt{secret});
550
551   FS::cust_main->API_update( %opt );
552 }
553
554 =item customer_info OPTION => VALUE, ...
555
556 Returns general customer information. Takes a list of keys and values as
557 parameters with the following keys: custnum, secret 
558
559 Example:
560
561   use Frontier::Client;
562   use Data::Dumper;
563
564   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
565                                               # the traffic
566
567   my $xmlrpc = new Frontier::Client url=>$url;
568
569   my $result = $xmlrpc->call( 'FS.API.customer_info',
570                                 'secret'  => 'sharingiscaring',
571                                 'custnum' => 181318,
572                             );
573
574   print Dumper($result);
575
576 Returns the following fields:
577
578 =over 4
579
580 =item error
581
582 Empty, or error message (in which case, none of the other fields will be populated)
583
584 =item display_custnum
585
586 Optional customer number display override - if present, use this for all UI instead of the real database custnum
587
588 =item name
589
590 Simple string for customer identification (from first, last, company)
591
592 =item balance
593
594 =item status
595
596 =item statuscolor
597
598 =item first
599
600 =item last
601
602 =item company
603
604 =item daytime
605
606 =item night
607
608 =item mobile
609
610 =item fax
611
612 =item agentnum
613
614 Agent (Company)
615
616 =item salesnum
617
618 Sales person
619
620 =item refnum
621
622 Advertising channel
623
624 =item classnum
625
626 Customer class
627
628 =item usernum
629
630 Employee (initial customer insert)
631
632 =item referral_custnum
633
634 Referring customer
635
636 =item address1
637
638 =item address2
639
640 =item city
641
642 =item county
643
644 =item state
645
646 =item zip
647
648 =item country
649
650 =item ship_address1
651
652 =item ship_address2
653
654 =item ship_city
655
656 =item ship_county
657
658 =item ship_state
659
660 =item ship_zip
661
662 =item ship_country
663
664 =item invoicing_list
665
666 Comma-separated list of email addresses
667
668 =item postal_invoicing
669
670 0 or 1
671
672 =back
673
674 =cut
675
676 sub customer_info {
677   my( $class, %opt ) = @_;
678   return _shared_secret_error() unless _check_shared_secret($opt{secret});
679
680   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
681     or return { 'error' => 'Unknown custnum' };
682
683   $cust_main->API_getinfo;
684 }
685
686 =item customer_list_svcs OPTION => VALUE, ...
687
688 Returns customer service information.  Takes a list of keys and values as
689 parameters with the following keys: custnum, secret
690
691 Example:
692
693   use Frontier::Client;
694   use Data::Dumper;
695
696   my $url = new URI 'http://localhost:8008/'; #or if accessing remotely, secure
697                                               # the traffic
698
699   my $xmlrpc = new Frontier::Client url=>$url;
700
701   my $result = $xmlrpc->call( 'FS.API.customer_list_svcs',
702                                 'secret'  => 'sharingiscaring',
703                                 'custnum' => 181318,
704                             );
705
706   print Dumper($result);
707
708   foreach my $cust_svc ( @{ $result->{'cust_svc'} } ) {
709     #print $cust_svc->{mac_addr}."\n" if exists $cust_svc->{mac_addr};
710     print $cust_svc->{circuit_id}."\n" if exists $cust_svc->{circuit_id};
711   }
712
713 =cut
714
715 sub customer_list_svcs {
716   my( $class, %opt ) = @_;
717   return _shared_secret_error() unless _check_shared_secret($opt{secret});
718
719   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
720     or return { 'error' => 'Unknown custnum' };
721
722   #$cust_main->API_list_svcs;
723
724   #false laziness w/ClientAPI/list_svcs
725
726   my @cust_svc = ();
727   #my @cust_pkg_usage = ();
728   #foreach my $cust_pkg ( $p->{'ncancelled'} 
729   #                       ? $cust_main->ncancelled_pkgs
730   #                       : $cust_main->unsuspended_pkgs ) {
731   foreach my $cust_pkg ( $cust_main->all_pkgs ) {
732     #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
733     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
734     #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
735   }
736
737   return {
738     'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
739   };
740
741 }
742
743 =item location_info
744
745 Returns location specific information for the customer. Takes a list of keys
746 and values as paramters with the following keys: custnum, secret
747
748 =cut
749
750 #I also monitor for changes to the additional locations that are applied to
751 # packages, and would like for those to be exportable as well.  basically the
752 # location data passed with the custnum.
753
754 sub location_info {
755   my( $class, %opt ) = @_;
756   return _shared_secret_error() unless _check_shared_secret($opt{secret});
757
758   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
759
760   my %return = (
761     'error'           => '',
762     'locations'       => [ map $_->hashref, @cust_location ],
763   );
764
765   return \%return;
766 }
767
768 =item list_customer_packages OPTION => VALUE, ...
769
770 Lists all customer packages.
771
772 =over
773
774 =item secret
775
776 API Secret
777
778 =item custnum
779
780 Customer Number
781
782 =back
783
784 Example:
785
786   my $result = FS::API->list_packages(
787     'secret'  => 'sharingiscaring',
788     'custnum'  => custnum,
789   );
790
791   if ( $result->{'error'} ) {
792     die $result->{'error'};
793   } else {
794     # list packages returns an array of hashes for packages ordered by custnum and pkgnum.
795     print Dumper($result->{'pkgs'});
796   }
797
798 =cut
799
800 sub list_customer_packages {
801   my( $class, %opt ) = @_;
802   return _shared_secret_error() unless _check_shared_secret($opt{secret});
803
804   my $sql_query = FS::cust_pkg->search({ 'custnum' => $opt{custnum}, });
805
806   $sql_query->{order_by} = 'ORDER BY custnum, pkgnum';
807
808   my @packages = qsearch($sql_query)
809     or return { 'error' => 'No packages' };
810
811   my $return = {
812     'packages'       => [ map $_->hashref, @packages ],
813   };
814
815   $return;
816 }
817
818 =item package_status OPTION => VALUE, ...
819
820 Get package status.
821
822 =over
823
824 =item secret
825
826 API Secret
827
828 =item pkgnum
829
830 Package Number
831
832 =back
833
834 Example:
835
836   my $result = FS::API->package_status(
837     'secret'  => 'sharingiscaring',
838     'pkgnum'  => pkgnum,
839   );
840
841   if ( $result->{'error'} ) {
842     die $result->{'error'};
843   } else {
844     # package status returns a hash with the status for a package.
845     print Dumper($result->{'status'});
846   }
847
848 =cut
849
850 sub package_status {
851   my( $class, %opt ) = @_;
852   return _shared_secret_error() unless _check_shared_secret($opt{secret});
853
854   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{pkgnum} } )
855     or return { 'error' => 'No packages' };
856
857   my $return = {
858     'status' => $cust_pkg->status,
859   };
860
861   $return;
862 }
863
864 =item order_package OPTION => VALUE, ...
865
866 Orders a new customer package.  Takes a list of keys and values as paramaters
867 with the following keys:
868
869 =over 4
870
871 =item secret
872
873 API Secret
874
875 =item custnum
876
877 =item pkgpart
878
879 =item quantity
880
881 =item start_date
882
883 =item contract_end
884
885 =item address1
886
887 =item address2
888
889 =item city
890
891 =item county
892
893 =item state
894
895 =item zip
896
897 =item country
898
899 =item setup_fee
900
901 Including this implements per-customer custom pricing for this package, overriding package definition pricing
902
903 =item recur_fee
904
905 Including this implements per-customer custom pricing for this package, overriding package definition pricing
906
907 =item invoice_details
908
909 A single string for just one detail line, or an array reference of one or more
910 lines of detail
911
912 =cut
913
914 sub order_package {
915   my( $class, %opt ) = @_;
916
917   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
918     or return { 'error' => 'Unknown custnum' };
919
920   #some conceptual false laziness w/cust_pkg/Import.pm
921
922   my $cust_pkg = new FS::cust_pkg {
923     'pkgpart'    => $opt{'pkgpart'},
924     'quantity'   => $opt{'quantity'} || 1,
925   };
926
927   #start_date and contract_end
928   foreach my $date_field (qw( start_date contract_end )) {
929     if ( $opt{$date_field} =~ /^(\d+)$/ ) {
930       $cust_pkg->$date_field( $opt{$date_field} );
931     } elsif ( $opt{$date_field} ) {
932       $cust_pkg->$date_field( str2time( $opt{$date_field} ) );
933     }
934   }
935
936   #especially this part for custom pkg price
937   # (false laziness w/cust_pkg/Import.pm)
938   my $s = $opt{'setup_fee'};
939   my $r = $opt{'recur_fee'};
940   my $part_pkg = $cust_pkg->part_pkg;
941   if (    ( length($s) && $s != $part_pkg->option('setup_fee') )
942        or ( length($r) && $r != $part_pkg->option('recur_fee') )
943      )
944   {
945
946     local($FS::part_pkg::skip_pkg_svc_hack) = 1;
947
948     my $custom_part_pkg = $part_pkg->clone;
949     $custom_part_pkg->disabled('Y');
950     my %options = $part_pkg->options;
951     $options{'setup_fee'} = $s if length($s);
952     $options{'recur_fee'} = $r if length($r);
953     my $error = $custom_part_pkg->insert( options=>\%options );
954     return ( 'error' => "error customizing package: $error" ) if $error;
955
956     #not ->pkg_svc, we want to ignore links and clone the actual package def
957     foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
958       my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
959       $c_pkg_svc->pkgsvcnum('');
960       $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
961       my $error = $c_pkg_svc->insert;
962       return "error customizing package: $error" if $error;
963     }
964
965     $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
966
967   }
968
969   my %order_pkg = ( 'cust_pkg' => $cust_pkg );
970
971   my @loc_fields = qw( address1 address2 city county state zip country );
972   if ( grep length($opt{$_}), @loc_fields ) {
973      $order_pkg{'cust_location'} = new FS::cust_location {
974        map { $_ => $opt{$_} } @loc_fields, 'custnum'
975      };
976   }
977
978   $order_pkg{'invoice_details'} = $opt{'invoice_details'}
979     if $opt{'invoice_details'};
980
981   my $error = $cust_main->order_pkg( %order_pkg );
982
983   #if ( $error ) {
984     return { 'error'  => $error,
985              #'pkgnum' => '',
986            };
987   #} else {
988   #  return { 'error'  => '',
989   #           #cust_main->order_pkg doesn't actually have a way to return pkgnum
990   #           #'pkgnum' => $pkgnum,
991   #         };
992   #}
993
994 }
995
996 =item change_package_location
997
998 Updates package location. Takes a list of keys and values 
999 as parameters with the following keys: 
1000
1001 pkgnum
1002
1003 secret
1004
1005 locationnum - pass this, or the following keys (don't pass both)
1006
1007 locationname
1008
1009 address1
1010
1011 address2
1012
1013 city
1014
1015 county
1016
1017 state
1018
1019 zip
1020
1021 addr_clean
1022
1023 country
1024
1025 censustract
1026
1027 censusyear
1028
1029 location_type
1030
1031 location_number
1032
1033 location_kind
1034
1035 incorporated
1036
1037 On error, returns a hashref with an 'error' key.
1038 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
1039 containing the new values.
1040
1041 =cut
1042
1043 sub change_package_location {
1044   my $class = shift;
1045   my %opt  = @_;
1046   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
1047
1048   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
1049     or return { 'error' => 'Unknown pkgnum' };
1050
1051   my %changeopt;
1052
1053   foreach my $field ( qw(
1054     locationnum
1055     locationname
1056     address1
1057     address2
1058     city
1059     county
1060     state
1061     zip
1062     addr_clean
1063     country
1064     censustract
1065     censusyear
1066     location_type
1067     location_number
1068     location_kind
1069     incorporated
1070   )) {
1071     $changeopt{$field} = $opt{$field} if $opt{$field};
1072   }
1073
1074   $cust_pkg->API_change(%changeopt);
1075 }
1076
1077 =item bill_now OPTION => VALUE, ...
1078
1079 Bills a single customer now, in the same fashion as the "Bill now" link in the
1080 UI.
1081
1082 Returns a hash reference with a single key, 'error'.  If there is an error,   
1083 the value contains the error, otherwise it is empty. Takes a list of keys and
1084 values as parameters with the following keys:
1085
1086 =over 4
1087
1088 =item secret
1089
1090 API Secret (required)
1091
1092 =item custnum
1093
1094 Customer number (required)
1095
1096 =back
1097
1098 =cut
1099
1100 sub bill_now {
1101   my( $class, %opt ) = @_;
1102   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1103
1104   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
1105     or return { 'error' => 'Unknown custnum' };
1106
1107   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
1108                                             'retry'      => 1,
1109                                             'check_freq' =>'1d',
1110                                           );
1111
1112    return { 'error' => $error,
1113           };
1114
1115 }
1116
1117
1118 #next.. Delete Advertising sources?
1119
1120 =item list_advertising_sources OPTION => VALUE, ...
1121
1122 Lists all advertising sources.
1123
1124 =over
1125
1126 =item secret
1127
1128 API Secret
1129
1130 =back
1131
1132 Example:
1133
1134   my $result = FS::API->list_advertising_sources(
1135     'secret'  => 'sharingiscaring',
1136   );
1137
1138   if ( $result->{'error'} ) {
1139     die $result->{'error'};
1140   } else {
1141     # list advertising sources returns an array of hashes for sources.
1142     print Dumper($result->{'sources'});
1143   }
1144
1145 =cut
1146
1147 #list_advertising_sources
1148 sub list_advertising_sources {
1149   my( $class, %opt ) = @_;
1150   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1151
1152   my @sources = qsearch('part_referral', {}, '', "")
1153     or return { 'error' => 'No referrals' };
1154
1155   my $return = {
1156     'sources'       => [ map $_->hashref, @sources ],
1157   };
1158
1159   $return;
1160 }
1161
1162 =item add_advertising_source OPTION => VALUE, ...
1163
1164 Add a new advertising source.
1165
1166 =over
1167
1168 =item secret
1169
1170 API Secret
1171
1172 =item referral
1173
1174 Referral name
1175
1176 =item disabled
1177
1178 Referral disabled, Y for disabled or nothing for enabled
1179
1180 =item agentnum
1181
1182 Agent ID number
1183
1184 =item title
1185
1186 External referral ID
1187
1188 =back
1189
1190 Example:
1191
1192   my $result = FS::API->add_advertising_source(
1193     'secret'     => 'sharingiscaring',
1194     'referral'   => 'test referral',
1195
1196     #optional
1197     'disabled'   => 'Y',
1198     'agentnum'   => '2', #agent id number
1199     'title'      => 'test title',
1200   );
1201
1202   if ( $result->{'error'} ) {
1203     die $result->{'error'};
1204   } else {
1205     # add_advertising_source returns new source upon success.
1206     print Dumper($result);
1207   }
1208
1209 =cut
1210
1211 #add_advertising_source
1212 sub add_advertising_source {
1213   my( $class, %opt ) = @_;
1214   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1215
1216   use FS::part_referral;
1217
1218   my $new_source = $opt{source};
1219
1220   my $source = new FS::part_referral $new_source;
1221
1222   my $error = $source->insert;
1223
1224   my $return = {$source->hash};
1225   $return = { 'error' => $error, } if $error;
1226
1227   $return;
1228 }
1229
1230 =item edit_advertising_source OPTION => VALUE, ...
1231
1232 Edit a advertising source.
1233
1234 =over
1235
1236 =item secret
1237
1238 API Secret
1239
1240 =item refnum
1241
1242 Referral number to edit
1243
1244 =item source
1245
1246 hash of edited source fields.
1247
1248 =over
1249
1250 =item referral
1251
1252 Referral name
1253
1254 =item disabled
1255
1256 Referral disabled, Y for disabled or nothing for enabled
1257
1258 =item agentnum
1259
1260 Agent ID number
1261
1262 =item title
1263
1264 External referral ID
1265
1266 =back
1267
1268 =back
1269
1270 Example:
1271
1272   my $result = FS::API->edit_advertising_source(
1273     'secret'     => 'sharingiscaring',
1274     'refnum'     => '4', # referral number to edit
1275     'source'     => {
1276        #optional
1277        'referral'   => 'test referral',
1278        'disabled'   => 'Y',
1279        'agentnum'   => '2', #agent id number
1280        'title'      => 'test title',
1281     }
1282   );
1283
1284   if ( $result->{'error'} ) {
1285     die $result->{'error'};
1286   } else {
1287     # edit_advertising_source returns updated source upon success.
1288     print Dumper($result);
1289   }
1290
1291 =cut
1292
1293 #edit_advertising_source
1294 sub edit_advertising_source {
1295   my( $class, %opt ) = @_;
1296   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1297
1298   use FS::part_referral;
1299
1300   my $refnum = $opt{refnum};
1301   my $source = $opt{source};
1302
1303   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1304   my $new = new FS::part_referral { $old->hash };
1305
1306   foreach my $key (keys %$source) {
1307     $new->$key($source->{$key});
1308   }
1309
1310   my $error = $new->replace;
1311
1312   my $return = {$new->hash};
1313   $return = { 'error' => $error, } if $error;
1314
1315   $return;
1316 }
1317
1318
1319 ##
1320 # helper subroutines
1321 ##
1322
1323 sub _check_shared_secret {
1324   shift eq FS::Conf->new->config('api_shared_secret');
1325 }
1326
1327 sub _shared_secret_error {
1328   return { 'error' => 'Incorrect shared secret' };
1329 }
1330
1331 1;