19a94638a0a0d2e9881a2e5112ea2a5340de4944
[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 #generally, the more useful data from the cust_main record the better.
216
217
218 # "2 way syncing" ?  start with non-sync pulling info here, then if necessary
219 # figure out how to trigger something when those things change
220
221 # long-term: package changes?
222
223 =item customer_info
224
225 =cut
226
227 #some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
228
229 use vars qw( @cust_main_editable_fields @location_editable_fields );
230 @cust_main_editable_fields = qw(
231   first last company daytime night fax mobile
232 );
233 #  locale
234 #  payby payinfo payname paystart_month paystart_year payissue payip
235 #  ss paytype paystate stateid stateid_state
236 @location_editable_fields = qw(
237   address1 address2 city county state zip country
238 );
239
240 sub customer_info {
241   my( $class, %opt ) = @_;
242   my $conf = new FS::Conf;
243   return { 'error' => 'Incorrect shared secret' }
244     unless $opt{secret} eq $conf->config('api_shared_secret');
245
246   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
247     or return { 'error' => 'Unknown custnum' };
248
249   my %return = (
250     'error'           => '',
251     'display_custnum' => $cust_main->display_custnum,
252     'name'            => $cust_main->first. ' '. $cust_main->get('last'),
253     'balance'         => $cust_main->balance,
254     'status'          => $cust_main->status,
255     'statuscolor'     => $cust_main->statuscolor,
256   );
257
258   $return{$_} = $cust_main->get($_)
259     foreach @cust_main_editable_fields;
260
261   for (@location_editable_fields) {
262     $return{$_} = $cust_main->bill_location->get($_)
263       if $cust_main->bill_locationnum;
264     $return{'ship_'.$_} = $cust_main->ship_location->get($_)
265       if $cust_main->ship_locationnum;
266   }
267
268   my @invoicing_list = $cust_main->invoicing_list;
269   $return{'invoicing_list'} =
270     join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
271   $return{'postal_invoicing'} =
272     0 < ( grep { $_ eq 'POST' } @invoicing_list );
273
274   return \%return;
275
276 }
277
278 #I also monitor for changes to the additional locations that are applied to
279 # packages, and would like for those to be exportable as well.  basically the
280 # location data passed with the custnum.
281 sub location_info {
282   my( $class, %opt ) = @_;
283   my $conf = new FS::Conf;
284   return { 'error' => 'Incorrect shared secret' }
285     unless $opt{secret} eq $conf->config('api_shared_secret');
286
287   my @cust_location = qsearch('cust_location', { 'custnum' => $opt{custnum} });
288
289   my %return = (
290     'error'           => '',
291     'locations'       => [ @cust_location ],
292   );
293
294   return \%return;
295 }
296
297 #Advertising sources?
298
299 =back
300
301 1;