File Coverage

blib/lib/Net/FreshBooks/API.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1 8     8   1892469 use strict;
  8         20  
  8         312  
2 8     8   49 use warnings;
  8         19  
  8         412  
3              
4             package Net::FreshBooks::API;
5             $Net::FreshBooks::API::VERSION = '0.24';
6 8     8   20025 use Moose;
  0            
  0            
7              
8             with 'Net::FreshBooks::API::Role::Common';
9              
10             #use namespace::autoclean;
11              
12             use Carp qw( carp croak );
13             use Data::Dump qw( dump );
14              
15             #use Devel::SimpleTrace;
16             use Net::FreshBooks::API::Client;
17             use Net::FreshBooks::API::Client::Contact;
18             use Net::FreshBooks::API::Estimate;
19             use Net::FreshBooks::API::Gateway;
20             use Net::FreshBooks::API::Invoice;
21             use Net::FreshBooks::API::OAuth;
22             use Net::FreshBooks::API::Language;
23             use Net::FreshBooks::API::Payment;
24             use Net::FreshBooks::API::Recurring;
25             use Path::Class;
26             use WWW::Mechanize;
27             use URI;
28              
29             has 'account_name' => ( is => 'rw' );
30             has 'auth_token' => ( is => 'rw' );
31             has 'api_version' => ( is => 'rw', default => 2.1 );
32             has 'auth_realm' => ( is => 'rw', default => 'FreshBooks' );
33             has 'ua' => ( is => 'rw', lazy_build => 1 );
34             has 'ua_name' => ( is => 'rw', lazy_build => 1 );
35              
36             # oauth methods
37             has 'access_token' => ( is => 'rw' );
38             has 'access_token_secret' => ( is => 'rw' );
39             has 'consumer_key' => ( is => 'rw' );
40             has 'consumer_secret' => ( is => 'rw' );
41              
42             sub ping {
43             my $self = shift;
44             eval { $self->client->list() };
45              
46             $self->_log( debug => $@ ? "ping failed: $@" : "ping succeeded" );
47             return if $@;
48             return 1;
49             }
50              
51             sub service_url {
52             my $self = shift;
53             my $account_name = $self->account_name || $self->consumer_key;
54              
55             croak "account_name required" if !$account_name;
56              
57             my $uri
58             = URI->new( 'https://'
59             . $account_name
60             . '.freshbooks.com/api/'
61             . $self->api_version
62             . '/xml-in' );
63              
64             return $uri;
65             }
66              
67             sub client {
68             return shift->_create_object( 'Client', @_ );
69             }
70              
71             sub estimate {
72             return shift->_create_object( 'Estimate', @_ );
73             }
74              
75             sub gateway {
76             return shift->_create_object( 'Gateway', @_ );
77             }
78              
79             sub invoice {
80             return shift->_create_object( 'Invoice', @_ );
81             }
82              
83             sub language {
84             return shift->_create_object( 'Language', @_ );
85             }
86              
87             sub payment {
88             return shift->_create_object( 'Payment', @_ );
89             }
90              
91             sub recurring {
92             return shift->_create_object( 'Recurring', @_ );
93             }
94              
95             sub _create_object {
96              
97             my $self = shift;
98             my $class = 'Net::FreshBooks::API::' . shift;
99              
100             my $args = shift || {};
101             my $obj = $class->new( _fb => $self, %$args );
102             $obj->verbose( $self->verbose );
103             $obj->die_on_server_error( $self->die_on_server_error );
104              
105             return $obj;
106              
107             }
108              
109             sub _build_ua_name {
110              
111             my $self = shift;
112             my $class = ref( $self ) || $self;
113             my $version = $Net::FreshBooks::API::VERSION || '0.00';
114              
115             return "$class (v$version)";
116              
117             }
118              
119             sub _build_ua {
120             my $self = shift;
121              
122             my $ua = LWP::UserAgent->new(
123             agent => $self->ua_name,
124             protocols_allowed => ['https'],
125             keep_alive => 10,
126             );
127              
128             # authenticate with and without realms
129             $ua->credentials( #
130             $self->service_url->host_port, # net loc
131             $self->auth_realm, # realm
132             $self->auth_token, # username
133             '' # password (none - all in username)
134             );
135              
136             $ua->credentials( #
137             $self->service_url->host_port, # net loc
138             '', # realm (none)
139             $self->auth_token, # username
140             '' # password (none - all in username)
141             );
142              
143             return $ua;
144             }
145              
146             sub delete_everything_from_this_test_account {
147              
148             my $self = shift;
149              
150             my $name = $self->account_name;
151             croak( "ERROR: account_name must end with 'test' to use"
152             . " the method delete_everything_on_this_test_account"
153             . " - your account name is '$name'" )
154             if ( $name !~ m{ test \z }x && $name ne 'netfreshbooksapi' );
155              
156             my $delete_count = 0;
157              
158             # note: 'payments' can't be deleted
159             my @names_to_delete = qw( invoice client );
160              
161             # clear out all existing clients etc on this account.
162             foreach my $object_name ( @names_to_delete ) {
163             my $objects_to_delete = $self->$object_name->list();
164             while ( my $obj = $objects_to_delete->next ) {
165             $obj->delete;
166             $delete_count++;
167             }
168             }
169              
170             return $delete_count;
171             }
172              
173             sub oauth {
174              
175             my $self = shift;
176              
177             my %tokens = (
178             consumer_key => $self->consumer_key || undef,
179             consumer_secret => $self->consumer_secret || undef,
180             account_name => $self->account_name || undef,
181             );
182              
183             if ( $self->access_token && $self->access_token_secret ) {
184             $tokens{'access_token'} = $self->access_token;
185             $tokens{'access_token_secret'} = $self->access_token_secret;
186             }
187              
188             my $oauth = Net::FreshBooks::API::OAuth->new( %tokens );
189              
190             return $oauth;
191              
192             }
193              
194             sub account_name_ok {
195              
196             my $self = shift;
197              
198             my $mech = WWW::Mechanize->new( autocheck => 0 );
199             $mech->agent( $self->ua_name );
200              
201             # FreshBooks redirects on all account names (valid or not)
202             $mech->requests_redirectable( [] );
203              
204             $mech->get( $self->service_url );
205              
206             # if your account name is valid, you'll get an "unauthorized" response
207             return ( $mech->status == 401 ) ? 1 : 0;
208              
209             }
210              
211             __PACKAGE__->meta->make_immutable();
212              
213             1;
214              
215             # ABSTRACT: Easy OO access to the FreshBooks.com API
216              
217             __END__
218              
219             =pod
220              
221             =encoding UTF-8
222              
223             =head1 NAME
224              
225             Net::FreshBooks::API - Easy OO access to the FreshBooks.com API
226              
227             =head1 VERSION
228              
229             version 0.24
230              
231             =head1 SYNOPSIS
232              
233             use Net::FreshBooks::API;
234              
235             # Authenticate with OAuth (recommended)
236             my $fb = Net::FreshBooks::API->new(
237             { consumer_key => $consumer_key, # your account_name
238             consumer_secret => $consumer_secret,
239             access_token => $access_token,
240             access_token_secret => $access_token_secret,
241             account_name => $account_name, # user's account name
242             }
243             );
244              
245              
246             # Or, use auth_token and account_name supplied by FreshBooks
247             my $fb = Net::FreshBooks::API->new(
248             { auth_token => $auth_token,
249             account_name => $account_name,
250             }
251             );
252              
253             # create a new client
254             my $client = $fb->client->create(
255             { first_name => 'Larry',
256             last_name => 'Wall',
257             organization => 'Perl HQ',
258             email => 'larry@example.com',
259             }
260             );
261              
262             # we can now make changes to the client and save them
263             $client->organization( 'Perl Foundation' );
264             $client->update;
265              
266             # or more quickly
267             $client->update( { organization => 'Perl Foundation', } );
268              
269             # create an invoice for this client
270             my $invoice = $fb->invoice(
271             { client_id => $client->client_id,
272             number => '00001',
273             }
274             );
275              
276             # add a line to the invoice
277             $invoice->add_line(
278             { name => 'Hawaiian shirt consulting',
279             unit_cost => 60,
280             quantity => 4,
281             }
282             );
283              
284             # save the invoice and then send it
285             $invoice->create;
286             $invoice->send_by_email;
287              
288             ############################################
289             # create a recurring item
290             ############################################
291              
292             use Net::FreshBooks::API;
293             use Net::FreshBooks::API::InvoiceLine;
294             use DateTime;
295              
296             # auth_token and account_name come from FreshBooks
297             my $fb = Net::FreshBooks::API->new(
298             { auth_token => $auth_token,
299             account_name => $account_name,
300             }
301             );
302              
303             # find the first client returned
304             my $client = $fb->client->list->next;
305              
306             # create a line item
307             my $line = Net::FreshBooks::API::InvoiceLine->new(
308             { name => "Widget",
309             description => "Net::FreshBooks::API Widget",
310             unit_cost => '1.99',
311             quantity => 1,
312             tax1_name => "GST",
313             tax1_percent => 5,
314             }
315             );
316              
317             # create the recurring item
318             my $recurring_item = $fb->recurring->create(
319             { client_id => $client->client_id,
320             date => DateTime->now->add( days => 2 )->ymd, # YYYY-MM-DD
321             frequency => 'monthly',
322             lines => [$line],
323             notes => 'Created by Net::FreshBooks::API',
324             }
325             );
326              
327             $recurring_item->po_number( 999 );
328             $recurring_item->update;
329              
330             See also L<Net::FreshBooks::API::Base> for other available methods, such as
331             create, update, get, list and delete.
332              
333             =head1 DESCRIPTION
334              
335             L<http://www.freshbooks.com> is a website that lets you create, send and
336             manage invoices. This module is an OO abstraction of their API that lets you
337             work with Clients, Invoices etc as if they were standard Perl objects.
338              
339             Repository: L<http://github.com/oalders/net-freshbooks-api/tree/master>
340              
341             =head2 OAUTH
342              
343             OAuth is the recommended method of authentication, but it can take a few days
344             for FreshBooks to approve your OAuth application. In the meantime, you can get
345             started right away by using an auth_token.
346              
347             Once your application has been approved, your consumer_key will be your
348             FreshBooks account name and your consumer_key_secret will be provided to you
349             by FreshBooks in your account. If you need to generate an access_token and
350             access_token_secret, you can so so by running the oauth.pl script in the
351             /examples directory which is included with this distribution.
352              
353             =head1 METHODS
354              
355             =head2 new
356              
357             Create a new API object using OAuth:
358              
359             my $fb = Net::FreshBooks::API->new(
360             { consumer_key => $consumer_key, # same as account_name
361             consumer_secret => $consumer_secret,
362             access_token => $access_token,
363             access_token_secret => $access_token_secret,
364             }
365             );
366              
367             Create a new API object the old (discouraged) way:
368              
369             # auth_token and account_name come from FreshBooks
370             my $fb = Net::FreshBooks::API->new(
371             { auth_token => $auth_token,
372             account_name => $account_name,
373             }
374             );
375              
376             =head2 client
377              
378             Returns a L<Net::FreshBooks::API::Client> object.
379              
380             =head2 estimate
381              
382             Creates and returns a new L<Net::FreshBooks::API::Estimate> object.
383              
384             =head2 gateway
385              
386             Creates and returns a new L<Net::FreshBooks::API::Gateway> object.
387              
388             =head2 invoice
389              
390             Creates and returns a new L<Net::FreshBooks::API::Invoice> object.
391              
392             =head2 language
393              
394             Creates and returns a new L<Net::FreshBooks::API::Language> object.
395              
396             =head2 payment
397              
398             Creates and returns a new L<Net::FreshBooks::API::Payment> object.
399              
400             =head2 recurring
401              
402             Creates and returns a new L<Net::FreshBooks::API::Recurring> object.
403              
404             =head2 ping
405              
406             my $bool = $fb->ping( );
407              
408             Ping the server with a trivial request to see if a connection can be made.
409             Returns true if the server is reachable and the authentication details are
410             valid.
411              
412             =head2 service_url
413              
414             my $url = $fb->service_url( );
415              
416             Returns a L<URI> object that represents the service URL.
417              
418             =head2 verbose
419              
420             Setting verbose to a true value will allow you to inspect the XML which is
421             being sent to FreshBooks
422              
423             =head2 ua
424              
425             my $ua = $fb->ua;
426              
427             Return a LWP::UserAgent object to use when contacting the server.
428              
429             =head2 delete_everything_from_this_test_account
430              
431             my $deletion_count
432             = $fb->delete_everything_from_this_test_account();
433              
434             Deletes all clients, invoices and payments from this account. This is
435             convenient when testing but potentially very dangerous. To prevent accidential
436             deletions this method has a very long name, and will croak if the account name
437             does not end with 'test'.
438              
439             As a general rule it is best to put this at the B<start> of your test scripts
440             rather than at the end. This will let you inspect your account at the end of
441             the test script to see what is left behind.
442              
443             =head1 OAUTH METHODS
444              
445             =head2 OAUTH ACCESSOR/MUTATOR METHODS
446              
447             The following OAuth methods are getter/setter methods, which can optionally also
448             be passed to new(). Required or optional is used in the context of OAuth
449             connections. If you are not connecting via OAuth then you can safely ignore
450             these options.
451              
452             =head3 account_name( $account_name )
453              
454             Required. Account name is the account name of the user who wishes to connect
455             to your app.
456              
457             For example, if "acmeinc" is attempting to connect to your "widgets" app:
458              
459             # acme usually logs in via https://acmeinc.freshbooks.com
460             $fb->account_name( 'acmeinc' );
461              
462             =head3 consumer_key( $consumer_key )
463              
464             Required. The consumer key will be provided to you by FreshBooks, but it's
465             generally just the name of your account.
466              
467             # account name is "mycompany"
468             # https://mycompany.freshbooks.com
469              
470             $fb->consumer_key( 'mycompany' );
471              
472             (In the case where you are logging in to your own app, consumer_key and
473             account_name will have the same value.)
474              
475             =head3 consumer_secret( $secret )
476              
477             Required. The consumer_secret is provided to you by FreshBooks. You'll need to
478             log in to your account to access it.
479              
480             =head3 access_token( $access_token )
481              
482             Optional. If you do not have an access_token, you'll need to acquire one
483             with your code and then set this parameter before you request restricted
484             URLs.
485              
486             =head3 access_token_secret( $access_token_secret )
487              
488             Optional. If you do not have an access_token_secret, you'll need to acquire
489             one with your code and then set this parameter before you request restricted
490             URLs.
491              
492             =head3 account_name_ok
493              
494             Returns true if $fb->account_name appears to be valid.
495              
496             =head2 OAUTH ACCESSOR METHODS
497              
498             =head2 oauth
499              
500             Returns a L<Net::FreshBooks::API::OAuth> object. This is a subclass of
501             L<Net::OAuth::Simple> See L<Net::FreshBooks::API::OAuth> as well as the
502             scripts in the /examples folder of this distribution for use cases.
503              
504             =head1 WARNING
505              
506             This code is still under development - any and all patches most welcome.
507              
508             The documentation is by no means complete. Feel free to look at the test files
509             for more examples of usage.
510              
511             Up to this point, only clients, invoices and recurring items have been
512             implemented, but other functionality may be added as needed. If you need other
513             details, they should be very easy to add. Please get in touch.
514              
515             =head1 AUTHOR CREDITS
516              
517             Edmund von der Burg C<<evdb@ecclestoad.co.uk>> (Original Author)
518              
519             Developed for HinuHinu L<http://www.hinuhinu.com/>.
520              
521             Recurring, Estimate and OAuth support by:
522              
523             Olaf Alders olaf@raybec.com
524              
525             Developed for Raybec Communications L<http://www.raybec.com>
526              
527             =head1 SEE ALSO
528              
529             L<WWW::FreshBooks::API> - an alternative interface to FreshBooks.
530              
531             L<http://developers.freshbooks.com> the FreshBooks API documentation.
532              
533             =head1 AUTHORS
534              
535             =over 4
536              
537             =item *
538              
539             Edmund von der Burg <evdb@ecclestoad.co.uk>
540              
541             =item *
542              
543             Olaf Alders <olaf@wundercounter.com>
544              
545             =back
546              
547             =head1 COPYRIGHT AND LICENSE
548              
549             This software is copyright (c) 2011 by Edmund von der Burg & Olaf Alders.
550              
551             This is free software; you can redistribute it and/or modify it under
552             the same terms as the Perl 5 programming language system itself.
553              
554             =cut