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