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