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