doc: return fields for customer_info, RT#84796
[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 Returns the following fields:
619
620 =over 4
621
622 =item error
623
624 Empty, or error message (in which case, none of the other fields will be populated)
625
626 =item display_custnum
627
628 Optional customer number display override - if present, use this for all UI instead of the real database custnum
629
630 =item name
631
632 Simple string for customer identification (from first, last, company)
633
634 =item balance
635
636 =item status
637
638 =item statuscolor
639
640 =item first
641
642 =item last
643
644 =item company
645
646 =item daytime
647
648 =item night
649
650 =item mobile
651
652 =item fax
653
654 =item agentnum
655
656 Agent (Company)
657
658 =item salesnum
659
660 Sales person
661
662 =item refnum
663
664 Advertising channel
665
666 =item classnum
667
668 Customer class
669
670 =item usernum
671
672 Employee (initial customer insert)
673
674 =item referral_custnum
675
676 Referring customer
677
678 =item address1
679
680 =item address2
681
682 =item city
683
684 =item county
685
686 =item state
687
688 =item zip
689
690 =item country
691
692 =item ship_address1
693
694 =item ship_address2
695
696 =item ship_city
697
698 =item ship_county
699
700 =item ship_state
701
702 =item ship_zip
703
704 =item ship_country
705
706 =item invoicing_list
707
708 Comma-separated list of email addresses
709
710 =item postal_invoicing
711
712 0 or 1
713
714 =back
715
716 =cut
717
718 sub update_customer {
719  my( $class, %opt ) = @_;
720   return _shared_secret_error() unless _check_shared_secret($opt{secret});
721
722   my $custnum = $opt{'custnum'}
723     or return { 'error' => "no customer record" };
724
725   my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
726     or return { 'error' => "unknown custnum $custnum" };
727
728   my $new = new FS::cust_main { $cust_main->hash };
729
730   $new->set( $_ => $opt{$_} )
731     foreach grep { exists $opt{$_} } qw(
732         agentnum salesnum refnum agent_custid referral_custnum
733         last first company
734         daytime night fax mobile
735         payby payinfo paydate paycvv payname
736       ),
737
738   my @invoicing_list;
739   if ( exists $opt{'invoicing_list'} || exists $opt{'postal_invoicing'} ) {
740     @invoicing_list = split( /\s*\,\s*/, $opt{'invoicing_list'} );
741     push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
742   } else {
743     @invoicing_list = $cust_main->invoicing_list;
744   }
745  
746   if ( exists( $opt{'address1'} ) ) {
747     my $bill_location = FS::cust_location->new({
748         map { $_ => $opt{$_} } @location_editable_fields
749     });
750     $bill_location->set('custnum' => $custnum);
751     my $error = $bill_location->find_or_insert;
752     die $error if $error;
753
754     # if this is unchanged from before, cust_main::replace will ignore it
755     $new->set('bill_location' => $bill_location);
756   }
757
758   if ( exists($opt{'ship_address1'}) && length($opt{"ship_address1"}) > 0 ) {
759     my $ship_location = FS::cust_location->new({
760         map { $_ => $opt{"ship_$_"} } @location_editable_fields
761     });
762
763     $ship_location->set('custnum' => $custnum);
764     my $error = $ship_location->find_or_insert;
765     die $error if $error;
766
767     $new->set('ship_location' => $ship_location);
768
769    } elsif (exists($opt{'ship_address1'} ) && !grep { length($opt{"ship_$_"}) } @location_editable_fields ) {
770       my $ship_location = $new->bill_location;
771      $new->set('ship_location' => $ship_location);
772     }
773
774   my $error = $new->replace( $cust_main, \@invoicing_list );
775   return { 'error'   => $error } if $error;
776
777   return { 'error'   => '',
778          };  
779 }
780
781
782 =item customer_info
783
784 Returns general customer information. Takes a list of keys and values as
785 parameters with the following keys: custnum, secret 
786
787 =cut
788
789 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
790
791 use vars qw( @cust_main_editable_fields @location_editable_fields );
792 @cust_main_editable_fields = qw(
793   first last company daytime night fax mobile
794 );
795 #  locale
796 #  payby payinfo payname paystart_month paystart_year payissue payip
797 #  ss paytype paystate stateid stateid_state
798 @location_editable_fields = qw(
799   address1 address2 city county state zip country
800 );
801
802 sub customer_info {
803   my( $class, %opt ) = @_;
804   return _shared_secret_error() unless _check_shared_secret($opt{secret});
805
806   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
807     or return { 'error' => 'Unknown custnum' };
808
809   my %return = (
810     'error'           => '',
811     'display_custnum' => $cust_main->display_custnum,
812     'name'            => $cust_main->first. ' '. $cust_main->get('last'),
813     'balance'         => $cust_main->balance,
814     'status'          => $cust_main->status,
815     'statuscolor'     => $cust_main->statuscolor,
816   );
817
818   $return{$_} = $cust_main->get($_)
819     foreach @cust_main_editable_fields;
820
821   for (@location_editable_fields) {
822     $return{$_} = $cust_main->bill_location->get($_)
823       if $cust_main->bill_locationnum;
824     $return{'ship_'.$_} = $cust_main->ship_location->get($_)
825       if $cust_main->ship_locationnum;
826   }
827
828   my @invoicing_list = $cust_main->invoicing_list;
829   $return{'invoicing_list'} =
830     join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
831   $return{'postal_invoicing'} =
832     0 < ( grep { $_ eq 'POST' } @invoicing_list );
833
834   #generally, the more useful data from the cust_main record the better.
835   # well, tell me what you want
836
837   return \%return;
838
839 }
840
841
842 =item customer_list_svcs OPTION => VALUE, ...
843
844 Returns customer service information.  Takes a list of keys and values as
845 parameters with the following keys: custnum, secret
846
847 =cut
848
849 sub customer_list_svcs {
850   my( $class, %opt ) = @_;
851   return _shared_secret_error() unless _check_shared_secret($opt{secret});
852
853   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
854     or return { 'error' => 'Unknown custnum' };
855
856   #$cust_main->API_list_svcs;
857
858   #false laziness w/ClientAPI/list_svcs
859
860   my @cust_svc = ();
861   #my @cust_pkg_usage = ();
862   #foreach my $cust_pkg ( $p->{'ncancelled'} 
863   #                       ? $cust_main->ncancelled_pkgs
864   #                       : $cust_main->unsuspended_pkgs ) {
865   foreach my $cust_pkg ( $cust_main->all_pkgs ) {
866     #next if $pkgnum && $cust_pkg->pkgnum != $pkgnum;
867     push @cust_svc, @{[ $cust_pkg->cust_svc ]}; #@{[ ]} to force array context
868     #push @cust_pkg_usage, $cust_pkg->cust_pkg_usage;
869   }
870
871   return {
872     'cust_svc' => [ map $_->API_getinfo, @cust_svc ],
873   };
874
875 }
876
877 =item location_info
878
879 Returns location specific information for the customer. Takes a list of keys
880 and values as paramters with the following keys: custnum, secret
881
882 =cut
883
884 #I also monitor for changes to the additional locations that are applied to
885 # packages, and would like for those to be exportable as well.  basically the
886 # location data passed with the custnum.
887
888 sub location_info {
889   my( $class, %opt ) = @_;
890   return _shared_secret_error() unless _check_shared_secret($opt{secret});
891
892   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
893
894   my %return = (
895     'error'           => '',
896     'locations'       => [ map $_->hashref, @cust_location ],
897   );
898
899   return \%return;
900 }
901
902 =item list_customer_packages OPTION => VALUE, ...
903
904 Lists all customer packages.
905
906 =over
907
908 =item secret
909
910 API Secret
911
912 =item custnum
913
914 Customer Number
915
916 =back
917
918 Example:
919
920   my $result = FS::API->list_packages(
921     'secret'  => 'sharingiscaring',
922     'custnum'  => custnum,
923   );
924
925   if ( $result->{'error'} ) {
926     die $result->{'error'};
927   } else {
928     # list packages returns an array of hashes for packages ordered by custnum and pkgnum.
929     print Dumper($result->{'pkgs'});
930   }
931
932 =cut
933
934 sub list_customer_packages {
935   my( $class, %opt ) = @_;
936   return _shared_secret_error() unless _check_shared_secret($opt{secret});
937
938   my $sql_query = FS::cust_pkg->search({ 'custnum' => $opt{custnum}, });
939
940   $sql_query->{order_by} = 'ORDER BY custnum, pkgnum';
941
942   my @packages = qsearch($sql_query)
943     or return { 'error' => 'No packages' };
944
945   my $return = {
946     'packages'       => [ map $_->hashref, @packages ],
947   };
948
949   $return;
950 }
951
952 =item package_status OPTION => VALUE, ...
953
954 Get package status.
955
956 =over
957
958 =item secret
959
960 API Secret
961
962 =item pkgnum
963
964 Package Number
965
966 =back
967
968 Example:
969
970   my $result = FS::API->package_status(
971     'secret'  => 'sharingiscaring',
972     'pkgnum'  => pkgnum,
973   );
974
975   if ( $result->{'error'} ) {
976     die $result->{'error'};
977   } else {
978     # package status returns a hash with the status for a package.
979     print Dumper($result->{'status'});
980   }
981
982 =cut
983
984 sub package_status {
985   my( $class, %opt ) = @_;
986   return _shared_secret_error() unless _check_shared_secret($opt{secret});
987
988   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{pkgnum} } )
989     or return { 'error' => 'No packages' };
990
991   my $return = {
992     'status' => $cust_pkg->status,
993   };
994
995   $return;
996 }
997
998 =item order_package OPTION => VALUE, ...
999
1000 Orders a new customer package.  Takes a list of keys and values as paramaters
1001 with the following keys:
1002
1003 =over 4
1004
1005 =item secret
1006
1007 API Secret
1008
1009 =item custnum
1010
1011 =item pkgpart
1012
1013 =item quantity
1014
1015 =item start_date
1016
1017 =item contract_end
1018
1019 =item address1
1020
1021 =item address2
1022
1023 =item city
1024
1025 =item county
1026
1027 =item state
1028
1029 =item zip
1030
1031 =item country
1032
1033 =item setup_fee
1034
1035 Including this implements per-customer custom pricing for this package, overriding package definition pricing
1036
1037 =item recur_fee
1038
1039 Including this implements per-customer custom pricing for this package, overriding package definition pricing
1040
1041 =item invoice_details
1042
1043 A single string for just one detail line, or an array reference of one or more
1044 lines of detail
1045
1046 =cut
1047
1048 sub order_package {
1049   my( $class, %opt ) = @_;
1050
1051   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
1052     or return { 'error' => 'Unknown custnum' };
1053
1054   #some conceptual false laziness w/cust_pkg/Import.pm
1055
1056   my $cust_pkg = new FS::cust_pkg {
1057     'pkgpart'    => $opt{'pkgpart'},
1058     'quantity'   => $opt{'quantity'} || 1,
1059   };
1060
1061   #start_date and contract_end
1062   foreach my $date_field (qw( start_date contract_end )) {
1063     if ( $opt{$date_field} =~ /^(\d+)$/ ) {
1064       $cust_pkg->$date_field( $opt{$date_field} );
1065     } elsif ( $opt{$date_field} ) {
1066       $cust_pkg->$date_field( str2time( $opt{$date_field} ) );
1067     }
1068   }
1069
1070   #especially this part for custom pkg price
1071   # (false laziness w/cust_pkg/Import.pm)
1072   my $s = $opt{'setup_fee'};
1073   my $r = $opt{'recur_fee'};
1074   my $part_pkg = $cust_pkg->part_pkg;
1075   if (    ( length($s) && $s != $part_pkg->option('setup_fee') )
1076        or ( length($r) && $r != $part_pkg->option('recur_fee') )
1077      )
1078   {
1079
1080     local($FS::part_pkg::skip_pkg_svc_hack) = 1;
1081
1082     my $custom_part_pkg = $part_pkg->clone;
1083     $custom_part_pkg->disabled('Y');
1084     my %options = $part_pkg->options;
1085     $options{'setup_fee'} = $s if length($s);
1086     $options{'recur_fee'} = $r if length($r);
1087     my $error = $custom_part_pkg->insert( options=>\%options );
1088     return ( 'error' => "error customizing package: $error" ) if $error;
1089
1090     #not ->pkg_svc, we want to ignore links and clone the actual package def
1091     foreach my $pkg_svc ( $part_pkg->_pkg_svc ) {
1092       my $c_pkg_svc = new FS::pkg_svc { $pkg_svc->hash };
1093       $c_pkg_svc->pkgsvcnum('');
1094       $c_pkg_svc->pkgpart( $custom_part_pkg->pkgpart );
1095       my $error = $c_pkg_svc->insert;
1096       return "error customizing package: $error" if $error;
1097     }
1098
1099     $cust_pkg->pkgpart( $custom_part_pkg->pkgpart );
1100
1101   }
1102
1103   my %order_pkg = ( 'cust_pkg' => $cust_pkg );
1104
1105   my @loc_fields = qw( address1 address2 city county state zip country );
1106   if ( grep length($opt{$_}), @loc_fields ) {
1107      $order_pkg{'cust_location'} = new FS::cust_location {
1108        map { $_ => $opt{$_} } @loc_fields, 'custnum'
1109      };
1110   }
1111
1112   $order_pkg{'invoice_details'} = $opt{'invoice_details'}
1113     if $opt{'invoice_details'};
1114
1115   my $error = $cust_main->order_pkg( %order_pkg );
1116
1117   #if ( $error ) {
1118     return { 'error'  => $error,
1119              #'pkgnum' => '',
1120            };
1121   #} else {
1122   #  return { 'error'  => '',
1123   #           #cust_main->order_pkg doesn't actually have a way to return pkgnum
1124   #           #'pkgnum' => $pkgnum,
1125   #         };
1126   #}
1127
1128 }
1129
1130 =item change_package_location
1131
1132 Updates package location. Takes a list of keys and values 
1133 as paramters with the following keys: 
1134
1135 pkgnum
1136
1137 secret
1138
1139 locationnum - pass this, or the following keys (don't pass both)
1140
1141 locationname
1142
1143 address1
1144
1145 address2
1146
1147 city
1148
1149 county
1150
1151 state
1152
1153 zip
1154
1155 addr_clean
1156
1157 country
1158
1159 censustract
1160
1161 censusyear
1162
1163 location_type
1164
1165 location_number
1166
1167 location_kind
1168
1169 incorporated
1170
1171 On error, returns a hashref with an 'error' key.
1172 On success, returns a hashref with 'pkgnum' and 'locationnum' keys,
1173 containing the new values.
1174
1175 =cut
1176
1177 sub change_package_location {
1178   my $class = shift;
1179   my %opt  = @_;
1180   return _shared_secret_error() unless _check_shared_secret($opt{'secret'});
1181
1182   my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $opt{'pkgnum'} })
1183     or return { 'error' => 'Unknown pkgnum' };
1184
1185   my %changeopt;
1186
1187   foreach my $field ( qw(
1188     locationnum
1189     locationname
1190     address1
1191     address2
1192     city
1193     county
1194     state
1195     zip
1196     addr_clean
1197     country
1198     censustract
1199     censusyear
1200     location_type
1201     location_number
1202     location_kind
1203     incorporated
1204   )) {
1205     $changeopt{$field} = $opt{$field} if $opt{$field};
1206   }
1207
1208   $cust_pkg->API_change(%changeopt);
1209 }
1210
1211 =item bill_now OPTION => VALUE, ...
1212
1213 Bills a single customer now, in the same fashion as the "Bill now" link in the
1214 UI.
1215
1216 Returns a hash reference with a single key, 'error'.  If there is an error,   
1217 the value contains the error, otherwise it is empty. Takes a list of keys and
1218 values as parameters with the following keys:
1219
1220 =over 4
1221
1222 =item secret
1223
1224 API Secret (required)
1225
1226 =item custnum
1227
1228 Customer number (required)
1229
1230 =back
1231
1232 =cut
1233
1234 sub bill_now {
1235   my( $class, %opt ) = @_;
1236   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1237
1238   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
1239     or return { 'error' => 'Unknown custnum' };
1240
1241   my $error = $cust_main->bill_and_collect( 'fatal'      => 'return',
1242                                             'retry'      => 1,
1243                                             'check_freq' =>'1d',
1244                                           );
1245
1246    return { 'error' => $error,
1247           };
1248
1249 }
1250
1251
1252 #next.. Delete Advertising sources?
1253
1254 =item list_advertising_sources OPTION => VALUE, ...
1255
1256 Lists all advertising sources.
1257
1258 =over
1259
1260 =item secret
1261
1262 API Secret
1263
1264 =back
1265
1266 Example:
1267
1268   my $result = FS::API->list_advertising_sources(
1269     'secret'  => 'sharingiscaring',
1270   );
1271
1272   if ( $result->{'error'} ) {
1273     die $result->{'error'};
1274   } else {
1275     # list advertising sources returns an array of hashes for sources.
1276     print Dumper($result->{'sources'});
1277   }
1278
1279 =cut
1280
1281 #list_advertising_sources
1282 sub list_advertising_sources {
1283   my( $class, %opt ) = @_;
1284   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1285
1286   my @sources = qsearch('part_referral', {}, '', "")
1287     or return { 'error' => 'No referrals' };
1288
1289   my $return = {
1290     'sources'       => [ map $_->hashref, @sources ],
1291   };
1292
1293   $return;
1294 }
1295
1296 =item add_advertising_source OPTION => VALUE, ...
1297
1298 Add a new advertising source.
1299
1300 =over
1301
1302 =item secret
1303
1304 API Secret
1305
1306 =item referral
1307
1308 Referral name
1309
1310 =item disabled
1311
1312 Referral disabled, Y for disabled or nothing for enabled
1313
1314 =item agentnum
1315
1316 Agent ID number
1317
1318 =item title
1319
1320 External referral ID
1321
1322 =back
1323
1324 Example:
1325
1326   my $result = FS::API->add_advertising_source(
1327     'secret'     => 'sharingiscaring',
1328     'referral'   => 'test referral',
1329
1330     #optional
1331     'disabled'   => 'Y',
1332     'agentnum'   => '2', #agent id number
1333     'title'      => 'test title',
1334   );
1335
1336   if ( $result->{'error'} ) {
1337     die $result->{'error'};
1338   } else {
1339     # add_advertising_source returns new source upon success.
1340     print Dumper($result);
1341   }
1342
1343 =cut
1344
1345 #add_advertising_source
1346 sub add_advertising_source {
1347   my( $class, %opt ) = @_;
1348   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1349
1350   use FS::part_referral;
1351
1352   my $new_source = $opt{source};
1353
1354   my $source = new FS::part_referral $new_source;
1355
1356   my $error = $source->insert;
1357
1358   my $return = {$source->hash};
1359   $return = { 'error' => $error, } if $error;
1360
1361   $return;
1362 }
1363
1364 =item edit_advertising_source OPTION => VALUE, ...
1365
1366 Edit a advertising source.
1367
1368 =over
1369
1370 =item secret
1371
1372 API Secret
1373
1374 =item refnum
1375
1376 Referral number to edit
1377
1378 =item source
1379
1380 hash of edited source fields.
1381
1382 =over
1383
1384 =item referral
1385
1386 Referral name
1387
1388 =item disabled
1389
1390 Referral disabled, Y for disabled or nothing for enabled
1391
1392 =item agentnum
1393
1394 Agent ID number
1395
1396 =item title
1397
1398 External referral ID
1399
1400 =back
1401
1402 =back
1403
1404 Example:
1405
1406   my $result = FS::API->edit_advertising_source(
1407     'secret'     => 'sharingiscaring',
1408     'refnum'     => '4', # referral number to edit
1409     'source'     => {
1410        #optional
1411        'referral'   => 'test referral',
1412        'disabled'   => 'Y',
1413        'agentnum'   => '2', #agent id number
1414        'title'      => 'test title',
1415     }
1416   );
1417
1418   if ( $result->{'error'} ) {
1419     die $result->{'error'};
1420   } else {
1421     # edit_advertising_source returns updated source upon success.
1422     print Dumper($result);
1423   }
1424
1425 =cut
1426
1427 #edit_advertising_source
1428 sub edit_advertising_source {
1429   my( $class, %opt ) = @_;
1430   return _shared_secret_error() unless _check_shared_secret($opt{secret});
1431
1432   use FS::part_referral;
1433
1434   my $refnum = $opt{refnum};
1435   my $source = $opt{source};
1436
1437   my $old = FS::Record::qsearchs('part_referral', {'refnum' => $refnum,});
1438   my $new = new FS::part_referral { $old->hash };
1439
1440   foreach my $key (keys %$source) {
1441     $new->$key($source->{$key});
1442   }
1443
1444   my $error = $new->replace;
1445
1446   my $return = {$new->hash};
1447   $return = { 'error' => $error, } if $error;
1448
1449   $return;
1450 }
1451
1452
1453 ##
1454 # helper subroutines
1455 ##
1456
1457 sub _check_shared_secret {
1458   shift eq FS::Conf->new->config('api_shared_secret');
1459 }
1460
1461 sub _shared_secret_error {
1462   return { 'error' => 'Incorrect shared secret' };
1463 }
1464
1465 1;