add salesnum to new_customer API, RT#33218
[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
11 =head1 NAME
12
13 FS::API - Freeside backend API
14
15 =head1 SYNOPSIS
16
17   use FS::API;
18
19 =head1 DESCRIPTION
20
21 This module implements a backend API for advanced back-office integration.
22
23 In contrast to the self-service API, which authenticates an end-user and offers
24 functionality to that end user, the backend API performs a simple shared-secret
25 authentication and offers full, administrator functionality, enabling
26 integration with other back-office systems.
27
28 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
29 the port by default, only allow access from back-office servers with the same
30 security precations as the Freeside server, and encrypt the communication
31 channel (for example, with an SSH tunnel or VPN) rather than accessing it
32 in plaintext.
33
34 =head1 METHODS
35
36 =over 4
37
38 =item insert_payment
39
40 Adds a new payment to a customers account. Takes a hash reference as parameter with the following keys:
41
42 =over 5
43
44 =item secret
45
46 API Secret
47
48 =item custnum
49
50 Customer number
51
52 =item payby
53
54 Payment type
55
56 =item paid
57
58 Amount paid
59
60 =item _date
61
62
63 Option date for payment
64
65 Example:
66
67   my $result = FS::API->insert_payment(
68     'secret'  => 'sharingiscaring',
69     'custnum' => 181318,
70     'payby'   => 'CASH',
71     'paid'    => '54.32',
72
73     #optional
74     '_date'   => 1397977200, #UNIX timestamp
75   );
76
77   if ( $result->{'error'} ) {
78     die $result->{'error'};
79   } else {
80     #payment was inserted
81     print "paynum ". $result->{'paynum'};
82   }
83
84 =back
85
86 =cut
87
88 #enter cash payment
89 sub insert_payment {
90   my($class, %opt) = @_;
91   my $conf = new FS::Conf;
92   return { 'error' => 'Incorrect shared secret' }
93     unless $opt{secret} eq $conf->config('api_shared_secret');
94
95   #less "raw" than this?  we are the backoffice API, and aren't worried
96   # about version migration ala cust_main/cust_location here
97   my $cust_pay = new FS::cust_pay { %opt };
98   my $error = $cust_pay->insert( 'manual'=>1 );
99   return { 'error'  => $error,
100            'paynum' => $cust_pay->paynum,
101          };
102 }
103
104 # pass the phone number ( from svc_phone ) 
105 sub insert_payment_phonenum {
106   my($class, %opt) = @_;
107   my $conf = new FS::Conf;
108   return { 'error' => 'Incorrect shared secret' }
109     unless $opt{secret} eq $conf->config('api_shared_secret');
110
111   $class->_by_phonenum('insert_payment', %opt);
112
113 }
114
115 sub _by_phonenum {
116   my($class, $method, %opt) = @_;
117   my $conf = new FS::Conf;
118   return { 'error' => 'Incorrect shared secret' }
119     unless $opt{secret} eq $conf->config('api_shared_secret');
120
121   my $phonenum = delete $opt{'phonenum'};
122
123   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
124     or return { 'error' => 'Unknown phonenum' };
125
126   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
127     or return { 'error' => 'Unlinked phonenum' };
128
129   $opt{'custnum'} = $cust_pkg->custnum;
130
131   $class->$method(%opt);
132
133 }
134
135 =item insert_credit
136
137 Adds a a credit to a customers account. Takes a hash reference as parameter with the following keys
138
139 =over 
140
141 =item secret
142
143 API Secret
144
145 =item custnum
146
147 customer number
148
149 =item amount
150
151 Amount of the credit
152
153 =item _date
154
155 The date the credit will be posted
156
157 Example:
158
159   my $result = FS::API->insert_credit(
160     'secret'  => 'sharingiscaring',
161     'custnum' => 181318,
162     'amount'  => '54.32',
163
164     #optional
165     '_date'   => 1397977200, #UNIX timestamp
166   );
167
168   if ( $result->{'error'} ) {
169     die $result->{'error'};
170   } else {
171     #credit was inserted
172     print "crednum ". $result->{'crednum'};
173   }
174
175 =back
176
177 =cut
178
179 #Enter credit
180 sub insert_credit {
181   my($class, %opt) = @_;
182   my $conf = new FS::Conf;
183   return { 'error' => 'Incorrect shared secret' }
184     unless $opt{secret} eq $conf->config('api_shared_secret');
185
186   $opt{'reasonnum'} ||= $conf->config('api_credit_reason');
187
188   #less "raw" than this?  we are the backoffice API, and aren't worried
189   # about version migration ala cust_main/cust_location here
190   my $cust_credit = new FS::cust_credit { %opt };
191   my $error = $cust_credit->insert;
192   return { 'error'  => $error,
193            'crednum' => $cust_credit->crednum,
194          };
195 }
196
197 # pass the phone number ( from svc_phone ) 
198 sub insert_credit_phonenum {
199   my($class, %opt) = @_;
200   my $conf = new FS::Conf;
201   return { 'error' => 'Incorrect shared secret' }
202     unless $opt{secret} eq $conf->config('api_shared_secret');
203
204   $class->_by_phonenum('insert_credit', %opt);
205
206 }
207
208 =item insert_refund
209
210 Adds a a credit to a customers account. Takes a hash reference as parameter with the following keys: custnum,payby,refund
211
212 Example:
213
214   my $result = FS::API->insert_refund(
215     'secret'  => 'sharingiscaring',
216     'custnum' => 181318,
217     'payby'   => 'CASH',
218     'refund'  => '54.32',
219
220     #optional
221     '_date'   => 1397977200, #UNIX timestamp
222   );
223
224   if ( $result->{'error'} ) {
225     die $result->{'error'};
226   } else {
227     #refund was inserted
228     print "refundnum ". $result->{'crednum'};
229   }
230
231 =cut
232
233 #Enter cash refund.
234 sub insert_refund {
235   my($class, %opt) = @_;
236   my $conf = new FS::Conf;
237   return { 'error' => 'Incorrect shared secret' }
238     unless $opt{secret} eq $conf->config('api_shared_secret');
239
240   # when github pull request #24 is merged,
241   #  will have to change over to default reasonnum like credit
242   # but until then, this will do
243   $opt{'reason'} ||= 'API refund';
244
245   #less "raw" than this?  we are the backoffice API, and aren't worried
246   # about version migration ala cust_main/cust_location here
247   my $cust_refund = new FS::cust_refund { %opt };
248   my $error = $cust_refund->insert;
249   return { 'error'     => $error,
250            'refundnum' => $cust_refund->refundnum,
251          };
252 }
253
254 # pass the phone number ( from svc_phone ) 
255 sub insert_refund_phonenum {
256   my($class, %opt) = @_;
257   my $conf = new FS::Conf;
258   return { 'error' => 'Incorrect shared secret' }
259     unless $opt{secret} eq $conf->config('api_shared_secret');
260
261   $class->_by_phonenum('insert_refund', %opt);
262
263 }
264
265 #---
266
267 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
268 # figure out how to trigger something when those things change
269
270 # long-term: package changes?
271
272 =item new_customer
273
274 Creates a new customer. Takes a hash reference as parameter with the following keys:
275
276 =over 4
277
278 =item secret
279
280 API Secret
281
282 =item first
283
284 first name (required)
285
286 =item last
287
288 last name (required)
289
290 =item ss
291
292 (not typically collected; mostly used for ACH transactions)
293
294 =item company
295
296 Company name
297
298 =item address1 (required)
299
300 Address line one
301
302 =item city (required)
303
304 City
305
306 =item county
307
308 County
309
310 =item state (required)
311
312 State
313
314 =item zip (required)
315
316 Zip or postal code
317
318 =item country
319
320 2 Digit Country Code
321
322 =item latitude
323
324 latitude
325
326 =item Longitude
327
328 longitude
329
330 =item geocode
331
332 Currently used for third party tax vendor lookups
333
334 =item censustract
335
336 Used for determining FCC 477 reporting
337
338 =item censusyear
339
340 Used for determining FCC 477 reporting
341
342 =item daytime
343
344 Daytime phone number
345
346 =item night
347
348 Evening phone number
349
350 =item fax
351
352 Fax number
353
354 =item mobile
355
356 Mobile number
357
358 =item invoicing_list
359
360 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),
361 postal_invoicing
362 Set to 1 to enable postal invoicing
363
364 =item payby
365
366 CARD, DCRD, CHEK, DCHK, LECB, BILL, COMP or PREPAY
367
368 =item payinfo
369
370 Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pin" for PREPAY, purchase order number for BILL
371
372 =item paycvv
373
374 Credit card CVV2 number (1.5+ or 1.4.2 with CVV schema patch)
375
376 =item paydate
377
378 Expiration date for CARD/DCRD
379
380 =item payname
381
382 Exact name on credit card for CARD/DCRD, bank name for CHEK/DCHK
383
384 =item referral_custnum
385
386 Referring customer number
387
388 =item salesnum
389
390 Sales person number
391
392 =item agentnum
393
394 Agent number
395
396 =item agent_custid
397
398 Agent specific customer number
399
400 =item referral_custnum
401
402 Referring customer number
403
404
405 =cut
406
407 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
408 # but approaching this from a clean start / back-office perspective
409 #  i.e. no package/service, no immediate credit card run, etc.
410
411 sub new_customer {
412   my( $class, %opt ) = @_;
413   my $conf = new FS::Conf;
414   return { 'error' => 'Incorrect shared secret' }
415     unless $opt{secret} eq $conf->config('api_shared_secret');
416
417   #default agentnum like signup_server-default_agentnum?
418  
419   #same for refnum like signup_server-default_refnum
420
421   my $cust_main = new FS::cust_main ( {
422       'agentnum' => $agentnum,
423       'refnum'   => $opt{refnum}
424                     || $conf->config('signup_server-default_refnum'),
425       'payby'    => 'BILL',
426       'tagnum'   => [ FS::part_tag->default_tags ],
427
428       map { $_ => $opt{$_} } qw(
429         agentnum salesnum refnum agent_custid referral_custnum
430         last first company 
431         daytime night fax mobile
432         payby payinfo paydate paycvv payname
433       ),
434
435   } );
436
437   my @invoicing_list = $opt{'invoicing_list'}
438                          ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
439                          : ();
440   push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
441
442   my ($bill_hash, $ship_hash);
443   foreach my $f (FS::cust_main->location_fields) {
444     # avoid having to change this in front-end code
445     $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
446     $ship_hash->{$f} = $opt{"ship_$f"};
447   }
448
449   my $bill_location = FS::cust_location->new($bill_hash);
450   my $ship_location;
451   # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
452   # so is there a ship address, and if so, is it different from the billing 
453   # address?
454   if ( length($ship_hash->{address1}) > 0 and
455           grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
456          ) {
457
458     $ship_location = FS::cust_location->new( $ship_hash );
459   
460   } else {
461     $ship_location = $bill_location;
462   }
463
464   $cust_main->set('bill_location' => $bill_location);
465   $cust_main->set('ship_location' => $ship_location);
466
467   $error = $cust_main->insert( {}, \@invoicing_list );
468   return { 'error'   => $error } if $error;
469   
470   return { 'error'   => '',
471            'custnum' => $cust_main->custnum,
472          };
473
474 }
475
476 =back 
477
478 =item customer_info
479
480 Returns general customer information. Takes a hash reference as parameter with the following keys: custnum and API secret 
481
482 =cut
483
484 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
485
486 use vars qw( @cust_main_editable_fields @location_editable_fields );
487 @cust_main_editable_fields = qw(
488   first last company daytime night fax mobile
489 );
490 #  locale
491 #  payby payinfo payname paystart_month paystart_year payissue payip
492 #  ss paytype paystate stateid stateid_state
493 @location_editable_fields = qw(
494   address1 address2 city county state zip country
495 );
496
497 sub customer_info {
498   my( $class, %opt ) = @_;
499   my $conf = new FS::Conf;
500   return { 'error' => 'Incorrect shared secret' }
501     unless $opt{secret} eq $conf->config('api_shared_secret');
502
503   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
504     or return { 'error' => 'Unknown custnum' };
505
506   my %return = (
507     'error'           => '',
508     'display_custnum' => $cust_main->display_custnum,
509     'name'            => $cust_main->first. ' '. $cust_main->get('last'),
510     'balance'         => $cust_main->balance,
511     'status'          => $cust_main->status,
512     'statuscolor'     => $cust_main->statuscolor,
513   );
514
515   $return{$_} = $cust_main->get($_)
516     foreach @cust_main_editable_fields;
517
518   for (@location_editable_fields) {
519     $return{$_} = $cust_main->bill_location->get($_)
520       if $cust_main->bill_locationnum;
521     $return{'ship_'.$_} = $cust_main->ship_location->get($_)
522       if $cust_main->ship_locationnum;
523   }
524
525   my @invoicing_list = $cust_main->invoicing_list;
526   $return{'invoicing_list'} =
527     join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
528   $return{'postal_invoicing'} =
529     0 < ( grep { $_ eq 'POST' } @invoicing_list );
530
531   #generally, the more useful data from the cust_main record the better.
532   # well, tell me what you want
533
534   return \%return;
535
536 }
537
538
539 =item location_info
540
541 Returns location specific information for the customer. Takes a hash reference as parameter with the following keys: custnum,secret
542
543 =back
544
545 =cut
546
547 #I also monitor for changes to the additional locations that are applied to
548 # packages, and would like for those to be exportable as well.  basically the
549 # location data passed with the custnum.
550
551 sub location_info {
552   my( $class, %opt ) = @_;
553   my $conf = new FS::Conf;
554   return { 'error' => 'Incorrect shared secret' }
555     unless $opt{secret} eq $conf->config('api_shared_secret');
556
557   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
558
559   my %return = (
560     'error'           => '',
561     'locations'       => [ map $_->hashref, @cust_location ],
562   );
563
564   return \%return;
565 }
566
567 #Advertising sources?
568
569
570 1;