spelling
[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 agentnum
389
390 Agent number
391
392 =item agent_custid
393
394 Agent specific customer number
395
396 =item referral_custnum
397
398 Referring customer number
399
400
401 =cut
402
403 #certainly false laziness w/ClientAPI::Signup new_customer/new_customer_minimal
404 # but approaching this from a clean start / back-office perspective
405 #  i.e. no package/service, no immediate credit card run, etc.
406
407 sub new_customer {
408   my( $class, %opt ) = @_;
409   my $conf = new FS::Conf;
410   return { 'error' => 'Incorrect shared secret' }
411     unless $opt{secret} eq $conf->config('api_shared_secret');
412
413   #default agentnum like signup_server-default_agentnum?
414  
415   #same for refnum like signup_server-default_refnum
416
417   my $cust_main = new FS::cust_main ( {
418       'agentnum'      => $agentnum,
419       'refnum'        => $opt{refnum}
420                          || $conf->config('signup_server-default_refnum'),
421       'payby'         => 'BILL',
422
423       map { $_ => $opt{$_} } qw(
424         agentnum refnum agent_custid referral_custnum
425         last first company 
426         daytime night fax mobile
427         payby payinfo paydate paycvv payname
428       ),
429
430   } );
431
432   my @invoicing_list = $opt{'invoicing_list'}
433                          ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
434                          : ();
435   push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
436
437   my ($bill_hash, $ship_hash);
438   foreach my $f (FS::cust_main->location_fields) {
439     # avoid having to change this in front-end code
440     $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
441     $ship_hash->{$f} = $opt{"ship_$f"};
442   }
443
444   my $bill_location = FS::cust_location->new($bill_hash);
445   my $ship_location;
446   # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
447   # so is there a ship address, and if so, is it different from the billing 
448   # address?
449   if ( length($ship_hash->{address1}) > 0 and
450           grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
451          ) {
452
453     $ship_location = FS::cust_location->new( $ship_hash );
454   
455   } else {
456     $ship_location = $bill_location;
457   }
458
459   $cust_main->set('bill_location' => $bill_location);
460   $cust_main->set('ship_location' => $ship_location);
461
462   $error = $cust_main->insert( {}, \@invoicing_list );
463   return { 'error'   => $error } if $error;
464   
465   return { 'error'   => '',
466            'custnum' => $cust_main->custnum,
467          };
468
469 }
470
471 =back 
472
473 =item customer_info
474
475 Returns general customer information. Takes a hash reference as parameter with the following keys: custnum and API secret 
476
477 =cut
478
479 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
480
481 use vars qw( @cust_main_editable_fields @location_editable_fields );
482 @cust_main_editable_fields = qw(
483   first last company daytime night fax mobile
484 );
485 #  locale
486 #  payby payinfo payname paystart_month paystart_year payissue payip
487 #  ss paytype paystate stateid stateid_state
488 @location_editable_fields = qw(
489   address1 address2 city county state zip country
490 );
491
492 sub customer_info {
493   my( $class, %opt ) = @_;
494   my $conf = new FS::Conf;
495   return { 'error' => 'Incorrect shared secret' }
496     unless $opt{secret} eq $conf->config('api_shared_secret');
497
498   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
499     or return { 'error' => 'Unknown custnum' };
500
501   my %return = (
502     'error'           => '',
503     'display_custnum' => $cust_main->display_custnum,
504     'name'            => $cust_main->first. ' '. $cust_main->get('last'),
505     'balance'         => $cust_main->balance,
506     'status'          => $cust_main->status,
507     'statuscolor'     => $cust_main->statuscolor,
508   );
509
510   $return{$_} = $cust_main->get($_)
511     foreach @cust_main_editable_fields;
512
513   for (@location_editable_fields) {
514     $return{$_} = $cust_main->bill_location->get($_)
515       if $cust_main->bill_locationnum;
516     $return{'ship_'.$_} = $cust_main->ship_location->get($_)
517       if $cust_main->ship_locationnum;
518   }
519
520   my @invoicing_list = $cust_main->invoicing_list;
521   $return{'invoicing_list'} =
522     join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
523   $return{'postal_invoicing'} =
524     0 < ( grep { $_ eq 'POST' } @invoicing_list );
525
526   #generally, the more useful data from the cust_main record the better.
527   # well, tell me what you want
528
529   return \%return;
530
531 }
532
533
534 =item location_info
535
536 Returns location specific information for the customer. Takes a hash reference as parameter with the following keys: custnum,secret
537
538 =back
539
540 =cut
541
542 #I also monitor for changes to the additional locations that are applied to
543 # packages, and would like for those to be exportable as well.  basically the
544 # location data passed with the custnum.
545
546 sub location_info {
547   my( $class, %opt ) = @_;
548   my $conf = new FS::Conf;
549   return { 'error' => 'Incorrect shared secret' }
550     unless $opt{secret} eq $conf->config('api_shared_secret');
551
552   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
553
554   my %return = (
555     'error'           => '',
556     'locations'       => [ map $_->hashref, @cust_location ],
557   );
558
559   return \%return;
560 }
561
562 #Advertising sources?
563
564
565 1;