API methods to pull customer information: name, phone numbers, email and postal billi...
[freeside.git] / FS / FS / API.pm
1 package FS::API;
2
3 use FS::Conf;
4 use FS::Record qw( qsearchs );
5 use FS::cust_main;
6 use FS::cust_pay;
7 use FS::cust_credit;
8 use FS::cust_refund;
9
10 =head1 NAME
11
12 FS::API - Freeside backend API
13
14 =head1 SYNOPSIS
15
16   use FS::API;
17
18 =head1 DESCRIPTION
19
20 This module implements a backend API for advanced back-office integration.
21
22 In contrast to the self-service API, which authenticates an end-user and offers
23 functionality to that end user, the backend API performs a simple shared-secret
24 authentication and offers full, administrator functionality, enabling
25 integration with other back-office systems.
26
27 If accessing this API remotely with XML-RPC or JSON-RPC, be careful to block
28 the port by default, only allow access from back-office servers with the same
29 security precations as the Freeside server, and encrypt the communication
30 channel (for exampple, with an SSH tunnel or VPN) rather than accessing it
31 in plaintext.
32
33 =head1 METHODS
34
35 =over 4
36
37 =item insert_payment
38
39 Example:
40
41   my $result = FS::API->insert_payment(
42     'secret'  => 'sharingiscaring',
43     'custnum' => 181318,
44     'payby'   => 'CASH',
45     'paid'    => '54.32',
46
47     #optional
48     '_date'   => 1397977200, #UNIX timestamp
49   );
50
51   if ( $result->{'error'} ) {
52     die $result->{'error'};
53   } else {
54     #payment was inserted
55     print "paynum ". $result->{'paynum'};
56   }
57
58 =cut
59
60 #enter cash payment
61 sub insert_payment {
62   my($class, %opt) = @_;
63   my $conf = new FS::Conf;
64   return { 'error' => 'Incorrect shared secret' }
65     unless $opt{secret} eq $conf->config('api_shared_secret');
66
67   #less "raw" than this?  we are the backoffice API, and aren't worried
68   # about version migration ala cust_main/cust_location here
69   my $cust_pay = new FS::cust_pay { %opt };
70   my $error = $cust_pay->insert( 'manual'=>1 );
71   return { 'error'  => $error,
72            'paynum' => $cust_pay->paynum,
73          };
74 }
75
76 # pass the phone number ( from svc_phone ) 
77 sub insert_payment_phonenum {
78   my($class, %opt) = @_;
79   my $conf = new FS::Conf;
80   return { 'error' => 'Incorrect shared secret' }
81     unless $opt{secret} eq $conf->config('api_shared_secret');
82
83   $class->_by_phonenum('insert_payment', %opt);
84
85 }
86
87 sub _by_phonenum {
88   my($class, $method, %opt) = @_;
89   my $conf = new FS::Conf;
90   return { 'error' => 'Incorrect shared secret' }
91     unless $opt{secret} eq $conf->config('api_shared_secret');
92
93   my $phonenum = delete $opt{'phonenum'};
94
95   my $svc_phone = qsearchs('svc_phone', { 'phonenum' => $phonenum } )
96     or return { 'error' => 'Unknown phonenum' };
97
98   my $cust_pkg = $svc_phone->cust_svc->cust_pkg
99     or return { 'error' => 'Unlinked phonenum' };
100
101   $opt{'custnum'} = $cust_pkg->custnum;
102
103   $class->$method(%opt);
104
105 }
106
107 =item insert_credit
108
109 Example:
110
111   my $result = FS::API->insert_credit(
112     'secret'  => 'sharingiscaring',
113     'custnum' => 181318,
114     'amount'  => '54.32',
115
116     #optional
117     '_date'   => 1397977200, #UNIX timestamp
118   );
119
120   if ( $result->{'error'} ) {
121     die $result->{'error'};
122   } else {
123     #credit was inserted
124     print "crednum ". $result->{'crednum'};
125   }
126
127 =cut
128
129 #Enter credit
130 sub insert_credit {
131   my($class, %opt) = @_;
132   my $conf = new FS::Conf;
133   return { 'error' => 'Incorrect shared secret' }
134     unless $opt{secret} eq $conf->config('api_shared_secret');
135
136   $opt{'reasonnum'} ||= $conf->config('api_credit_reason');
137
138   #less "raw" than this?  we are the backoffice API, and aren't worried
139   # about version migration ala cust_main/cust_location here
140   my $cust_credit = new FS::cust_credit { %opt };
141   my $error = $cust_credit->insert;
142   return { 'error'  => $error,
143            'crednum' => $cust_credit->crednum,
144          };
145 }
146
147 # pass the phone number ( from svc_phone ) 
148 sub insert_credit_phonenum {
149   my($class, %opt) = @_;
150   my $conf = new FS::Conf;
151   return { 'error' => 'Incorrect shared secret' }
152     unless $opt{secret} eq $conf->config('api_shared_secret');
153
154   $class->_by_phonenum('insert_credit', %opt);
155
156 }
157
158 =item insert_refund
159
160 Example:
161
162   my $result = FS::API->insert_refund(
163     'secret'  => 'sharingiscaring',
164     'custnum' => 181318,
165     'payby'   => 'CASH',
166     'refund'  => '54.32',
167
168     #optional
169     '_date'   => 1397977200, #UNIX timestamp
170   );
171
172   if ( $result->{'error'} ) {
173     die $result->{'error'};
174   } else {
175     #refund was inserted
176     print "refundnum ". $result->{'crednum'};
177   }
178
179 =cut
180
181 #Enter cash refund.
182 sub insert_refund {
183   my($class, %opt) = @_;
184   my $conf = new FS::Conf;
185   return { 'error' => 'Incorrect shared secret' }
186     unless $opt{secret} eq $conf->config('api_shared_secret');
187
188   # when github pull request #24 is merged,
189   #  will have to change over to default reasonnum like credit
190   # but until then, this will do
191   $opt{'reason'} ||= 'API refund';
192
193   #less "raw" than this?  we are the backoffice API, and aren't worried
194   # about version migration ala cust_main/cust_location here
195   my $cust_refund = new FS::cust_refund { %opt };
196   my $error = $cust_refund->insert;
197   return { 'error'     => $error,
198            'refundnum' => $cust_refund->refundnum,
199          };
200 }
201
202 # pass the phone number ( from svc_phone ) 
203 sub insert_refund_phonenum {
204   my($class, %opt) = @_;
205   my $conf = new FS::Conf;
206   return { 'error' => 'Incorrect shared secret' }
207     unless $opt{secret} eq $conf->config('api_shared_secret');
208
209   $class->_by_phonenum('insert_refund', %opt);
210
211 }
212
213 #---
214
215 #Customer data
216 # pull customer info 
217 # The fields needed are:
218 #
219 # cust_main.custnum
220 # cust_main.first
221 # cust_main.last
222 # cust_main.company
223 # cust_main.address1
224 # cust_main.address2
225 # cust_main.city
226 # cust_main.state
227 # cust_main.zip
228 # cust_main.daytime
229 # cust_main.night
230 # cust_main_invoice.dest
231 #
232 # at minimum
233
234 #Customer balances
235
236 #Advertising sources?
237
238 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
239 # figure out how to trigger something when those things change
240
241 # long-term: package changes?
242
243 =item customer_info
244
245 =cut
246
247 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
248
249 use vars qw( @cust_main_editable_fields @location_editable_fields );
250 @cust_main_editable_fields = qw(
251   first last company daytime night fax mobile
252 );
253 #  locale
254 #  payby payinfo payname paystart_month paystart_year payissue payip
255 #  ss paytype paystate stateid stateid_state
256 @location_editable_fields = qw(
257   address1 address2 city county state zip country
258 );
259
260 sub customer_info {
261   my( $class, %opt ) = @_;
262   my $conf = new FS::Conf;
263   return { 'error' => 'Incorrect shared secret' }
264     unless $opt{secret} eq $conf->config('api_shared_secret');
265
266   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
267     or return { 'error' => 'Unknown custnum' };
268
269   my %return = (
270     'error'           => '',
271     'display_custnum' => $cust_main->display_custnum,
272     'name'            => $cust_main->first. ' '. $cust_main->get('last'),
273   );
274
275   $return{$_} = $cust_main->get($_)
276     foreach @cust_main_editable_fields;
277
278   for (@location_editable_fields) {
279     $return{$_} = $cust_main->bill_location->get($_)
280       if $cust_main->bill_locationnum;
281     $return{'ship_'.$_} = $cust_main->ship_location->get($_)
282       if $cust_main->ship_locationnum;
283   }
284
285   my @invoicing_list = $cust_main->invoicing_list;
286   $return{'invoicing_list'} =
287     join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
288   $return{'postal_invoicing'} =
289     0 < ( grep { $_ eq 'POST' } @invoicing_list );
290
291   return \%return;
292
293 }
294
295 =back
296
297 1;