File Coverage

blib/lib/Business/Tax/Avalara.pm
Criterion Covered Total %
statement 37 172 21.5
branch 1 46 2.1
condition 3 29 10.3
subroutine 11 26 42.3
pod 6 6 100.0
total 58 279 20.7


line stmt bran cond sub pod time code
1             package Business::Tax::Avalara;
2              
3 5     5   120875 use 5.010;
  5         20  
  5         280  
4              
5 5     5   38 use strict;
  5         11  
  5         181  
6 5     5   27 use warnings;
  5         15  
  5         143  
7              
8 5     5   4868 use Try::Tiny;
  5         8224  
  5         294  
9 5     5   33 use Carp;
  5         10  
  5         254  
10 5     5   4457 use LWP;
  5         285802  
  5         180  
11 5     5   5084 use HTTP::Request::Common;
  5         11157  
  5         487  
12 5     5   5093 use Encode qw();
  5         72824  
  5         140  
13 5     5   4725 use Data::Dump;
  5         41777  
  5         439  
14 5     5   7465 use JSON::PP;
  5         78801  
  5         12491  
15              
16              
17             =head1 NAME
18              
19             Business::Tax::Avalara - An interface to Avalara's REST webservice
20              
21             =head1 SYNOPSYS
22              
23             use Business::Tax::Avalara;
24             my $avalara_gateway = Business::Tax::Avalara->new(
25             customer_code => $customer_code,
26             company_code => $company_code,
27             user_name => $user_name,
28             password => $password,
29             origin_address =>
30             {
31             line_1 => '1313 Mockingbird Lane',
32             postal_code => '98765',
33             },
34             );
35            
36             my $tax_results = $avalara_gateway->get_tax(
37             destination_address =>
38             {
39             line_1 => '42 Evergreen Terrace',
40             city => 'Springfield',
41             postal_code => '12345',
42             },
43             cart_lines =>
44             [
45             {
46             sku => '42ACE',
47             quantity => 1,
48             amount => '8.99',
49             },
50             {
51             sku => '9FCE2',
52             quantity => 2,
53             amount => '38.98',
54             }
55             ],
56            
57             );
58            
59              
60             =head1 DESCRIPTION
61              
62             Business::Tax::Avalara is a simple interface to Avalara's REST-based sales tax webservice.
63             It takes in a perl hash of data to send to Avalara, generates the JSON, fetches a response,
64             and converts that back into a perl hash structure.
65              
66             This module only supports the 'get_tax' method at the moment.
67              
68             =head1 VERSION
69              
70             Version 1.1.0
71              
72             =cut
73              
74             our $VERSION = '1.1.0';
75             our $AVALARA_REQUEST_SERVER = 'rest.avalara.net';
76             our $AVALARA_DEVELOPMENT_REQUEST_SERVER = 'development.avalara.net';
77              
78            
79             =head1 FUNCTIONS
80              
81             =head2 new()
82              
83             Creates a new Business::Tax::Avalara object with various options that do not change
84             between requests.
85              
86             my $avalara_gateway = Business::Tax::Avalara->new(
87             customer_code => $customer_code,
88             company_code => $company_code,
89             user_name => $user_name
90             pasword => $password,
91             is_development => boolean (optional), default 0
92             origin_address => $origin_address (optional),
93             memcached => A Cache::Memcached or Cache::Memcached::Fast object.
94             request_timeout => Request timeout in seconds. Default is 3.
95             debug => 0,
96             );
97            
98             The fields customer_code, company_code, user_name, and password should be
99             provided by your Avalara representative. Account number and License key
100             are synonyms for user_name and password, respectively.
101              
102             is_development should be set to 1 to use the development URL, and 0 for
103             production uses.
104              
105             origin_address can either be set here, or passed into get_tax, depending on if
106             it changes per request, or if you're always shipping from the same location.
107             It is a hash ref, see below for formatting details.
108              
109             If a memcached object is passed in, we can use this so that we don't send the same
110             request over in a certain period of time. This combines below with 'cache_timespan'
111             and 'unique_key' in the get_tax() call.
112              
113             If debug is set to a true value, it will dump out the raw json messages being sent to
114             and coming back from Avalara.
115              
116             Returns a Business::Tax::Avalara object.
117              
118             =cut
119              
120             sub new
121             {
122 1     1 1 28 my ( $class, %args ) = @_;
123            
124 1         9 my @required_fields = qw( customer_code company_code user_name password );
125 1         3 foreach my $required_field ( @required_fields )
126             {
127 4 50       16 if ( !defined $args{ $required_field } )
128             {
129 0         0 croak "Could not instantiate Business::Tax::Avalara module: Required field >$required_field< is missing.";
130             }
131             }
132            
133 1   50     33 my $self = {
      50        
      50        
134             customer_code => $args{'customer_code'},
135             company_code => $args{'company_code'},
136             is_development => $args{'is_development'} // 0,
137             user_name => $args{'user_name'},
138             password => $args{'password'},
139             origin_address => $args{'origin_address'},
140             request_timeout => $args{'request_timeout'} // 3,
141             debug => $args{'debug'} // 0,
142             };
143            
144 1         4 bless $self, $class;
145 1         6 return $self;
146             }
147              
148              
149             =head2 get_tax()
150              
151             Makes a JSON request using the 'get_tax' method, parses the response, and returns a perl hash.
152              
153             my $tax_results = $avalara_gateway->get_tax(
154             destination_address => $address_hash,
155             origin_address => $address_hash (may be specified in new),
156             document_date => $date (optional), default is current date
157             cart_lines => $cart_line_hash,
158             customer_usage_type => $customer_usage_type (optional),
159             discount => $order_level_discount (optional),
160             purchase_order_number => $purchase_order_number (optional),
161             exemption_number => $exemption_number (optional),
162             detail_level => $detail_level (optional), default 'Tax',
163             document_type => $document_type (optional), default 'SalesOrder'
164             document_code => $document_code (optional), a unique identifier
165             payment_date => $date (optional),
166             reference_code => $reference_code (optional),
167             commit => 1|0, # Default 0, whether this is a 'final' query.
168             unique_key => A unique key for memcache (optional, see below)
169             cache_timespan => The number of seconds to cache results (see below),
170             doc_code => Unique document code (optional),
171             );
172              
173             See below for the definitions of address and cart_line fields. The field origin_address
174             may be specified here if it changes between transactions, or in new if it's largely static.
175              
176             detail level is one of 'Tax', 'Summary', 'Document', 'Line', or 'Diagnostic'.
177             See the Avalara documentation for the distinctions.
178              
179             document_type is one of 'SalesOrder', 'SalesInvoice', 'PurchaseOrder', 'PurchaseInvoice',
180             'ReturnOrder', and 'ReturnInvoice'.
181              
182             document_code is optional, but highly recommended. If you do not include this,
183             Avalara will generate a new internal unique id for each request, and it does not
184             associate the commits to any queries you made along the way.
185              
186             If cache_timespan is set and you passed a memcached object into new(), it will attempt
187             to cache the result based on the unique key passed in.
188              
189             Returns a perl hashref based on the Avalara return.
190             See the Avalara documentation for the full description of the output, but the highlights are:
191              
192             {
193             ResultCode => 'Success',
194             TaxAddresses => [ array of address information ],
195             TaxDate => Date,
196             TaxLines =>
197             {
198             LineNumber => # The value of the line number
199             {
200             Discount => Discount,
201             LineNo => Line Number passed in,
202             Rate => Tax rate used,
203             Tax => Line item tax
204             Taxability => "true" or "false",
205             Taxable => Amount taxable,
206             TaxCalculated => Line item tax
207             TaxCode => Tax Code used in the calculation
208             Tax Details => Details about state, county, city components of the tax
209            
210             },
211             ...
212             },
213             Timestamp => Timestamp,
214             TotalAmount => Total amount before tax
215             TotalDiscount => Total Discount
216             TotalExemption => Total amount exempt
217             TotalTax => Tax for the whole order
218             TotalTaxable => Amount that's taxable
219             }
220             =cut
221              
222             sub get_tax
223             {
224 0     0 1   my ( $self, %args ) = @_;
225            
226 0           my $unique_key = delete $args{'unique_key'};
227 0           my $cache_timespan = delete $args{'cache_timespan'};
228            
229             # Perl output, aka a hash ref, as opposed to JSON.
230 0           my $tax_perl_output = undef;
231            
232 0 0         if ( defined( $cache_timespan ) )
233             {
234             # Cache event.
235 0           $tax_perl_output = $self->get_cache(
236             key => $unique_key,
237             );
238             }
239            
240 0 0         if ( !defined $tax_perl_output )
241             {
242             # It wasn't in the cache or we aren't using cache, go get it.
243             try
244             {
245 0     0     my $request_json = $self->_generate_request_json( %args );
246 0           my $result_json = $self->_make_request_json( $request_json );
247 0           $tax_perl_output = $self->_parse_response_json( $result_json );
248             }
249             catch
250             {
251 0     0     carp( "Failed to fetch Avalara tax information: ", $_ );
252 0           return;
253 0           };
254            
255 0 0         if ( defined( $cache_timespan ) )
256             {
257             # Set it in the cache.
258 0           $self->set_cache(
259             key => $unique_key,
260             value => $tax_perl_output,
261             expire_time => $cache_timespan,
262             );
263             }
264             }
265 0           return $tax_perl_output;
266             }
267              
268              
269             =head2 cancel_tax()
270              
271             Makes a JSON request using the 'cancel_tax' method, parses the response, and returns a perl hash.
272              
273             my $tax_results = $avalara_gateway->cancel_tax(
274             document_type => $document_type, default 'SalesOrder'
275             doc_code => $doc_code,
276             cancel_code => $cancel_code, default 'DocVoided',
277             doc_id => $doc_id,
278             );
279              
280             Either doc_id (which is Avalara's transaction ID returned from get_tax() )
281             or the combination of document_type, doc_coe, and doc_id are required.
282              
283             Returns a perl hashref based on the Avalara return.
284             See the Avalara documentation for the full description of the output, but the highlights are:
285              
286             'CancelTaxResult' =>
287             {
288             ResultCode => 'Success',
289             DocID => SomeDocID,
290             TransactionID => Avalara's ID,
291             }
292              
293             =cut
294              
295             sub cancel_tax
296             {
297 0     0 1   my ( $self, %args ) = @_;
298            
299 0   0       my $document_type = delete $args{'document_type'} // 'SalesOrder';
300 0           my $doc_code = delete $args{'doc_code'};
301 0   0       my $cancel_code = delete $args{'cancel_code'} // 'DocVoided';
302 0           my $doc_id = delete $args{'doc_id'};
303            
304             # We need either doc_id or doc_code and document_type and cancel_code.
305             # But there are defaults on document_type and cancel type, so really
306             # we just need doc_id or doc_code.
307 0 0 0       if ( !defined $doc_id && !defined $doc_code )
308             {
309 0           carp( "Either a doc_id, or the combination of doc_code, cancel_code, and document_type is required." );
310 0           return undef;
311             }
312              
313 0           my %request;
314 0 0         if ( defined $doc_id )
315             {
316 0           $request{'doc_id'} = $doc_id;
317             }
318             else
319             {
320 0           $request{'document_type'} = $document_type;
321 0           $request{'doc_code'} = $doc_code;
322 0           $request{'cancel_code'} = $cancel_code;
323             }
324            
325             my $cancel_output = try
326             {
327 0     0     my $request_json = $self->_generate_cancel_request_json( %request );
328 0           my $result_json = $self->_make_request_json( $request_json, 'cancel' );
329 0           return $self->_parse_response_json( $result_json );
330             }
331             catch
332             {
333 0     0     carp( "Failed to cancel Avalara tax record: ", $_ );
334 0           return undef;
335 0           };
336              
337 0           return $cancel_output;
338             }
339              
340              
341             =head1 INTERNAL FUNCTIONS
342              
343             =head2 _generate_request_json()
344              
345             Generates the json to send to Avalara's web service.
346              
347             Returns a JSON object.
348              
349             =cut
350              
351             sub _generate_request_json
352             {
353 0     0     my ( $self, %args ) = @_;
354            
355             # Add in all the required elements.
356 0           my @now = localtime();
357 0 0         my $doc_date = defined $args{'doc_date'}
358             ? $args{'doc_date'}
359             : sprintf( "%4d-%02d-%02d", $now[5] + 1900, $now[4] + 1, $now[3] );
360              
361 0 0 0       my $request =
362             {
363             DocDate => $doc_date,
364             CustomerCode => $self->{'customer_code'},
365             CompanyCode => $self->{'company_code'},
366             Commit => ( $args{'commit'} // 0 ) ? 'true' : 'false',
367             };
368            
369 0           $request->{'Addresses'} = [ $self->_generate_address_json( $args{'destination_address'}, 1 ) ];
370 0   0       push @{ $request->{'Addresses'} },
  0            
371             $self->_generate_address_json( $self->{'origin_address'} // $args{'origin_address'}, 2 );
372            
373 0           $request->{'Lines'} = [];
374            
375 0           my $counter = 1;
376 0           foreach my $cart_line ( @{ $args{'cart_lines'} } )
  0            
377             {
378 0           push @{ $request->{'Lines'} }, $self->_generate_cart_line_json( $cart_line, $counter );
  0            
379 0           $counter++;
380             }
381            
382 0           my %optional_nodes =
383             (
384             customer_usage_type => 'CustomerUsageType',
385             discount => 'Discount',
386             purchase_order_number => 'PurchaseOrderNo',
387             exemption_number => 'ExemptionNo',
388             detail_level => 'DetailLevel',
389             document_type => 'DocType',
390             payment_date => 'PaymentDate',
391             reference_code => 'ReferenceCode',
392             document_code => 'DocCode',
393             );
394            
395 0           foreach my $node_name ( keys %optional_nodes )
396             {
397 0 0         next if ( !defined $args{ $node_name } );
398 0           $request->{ $optional_nodes{ $node_name } } = $args{ $node_name };
399             }
400            
401 0           my $json = JSON::PP->new()->ascii()->pretty()->allow_nonref();
402 0           return $json->encode( $request );
403             }
404              
405              
406             =head2 _generate_cancel_request_json()
407              
408             Generates the json to cancel a tax request to Avalara's web service.
409              
410             Returns a JSON object.
411              
412             =cut
413              
414             sub _generate_cancel_request_json
415             {
416 0     0     my ( $self, %args ) = @_;
417            
418 0           my $request =
419             {
420             CompanyCode => $self->{'company_code'},
421             };
422            
423 0           my %optional_nodes =
424             (
425             document_type => 'DocType',
426             doc_code => 'DocCode',
427             cancel_code => 'CancelCode',
428             doc_id => 'DocId',
429             );
430            
431 0           foreach my $node_name ( keys %optional_nodes )
432             {
433 0 0         next if ( !defined $args{ $node_name } );
434 0           $request->{ $optional_nodes{ $node_name } } = $args{ $node_name };
435             }
436              
437 0           my $json = JSON::PP->new()->ascii()->pretty()->allow_nonref();
438 0           return $json->encode( $request );
439             }
440              
441              
442             =head2 _generate_address_json()
443              
444             Given an address hashref, generates and returns a data structure to be converted to JSON.
445              
446             An address hashref is defined as:
447              
448             my $address = {
449             line_1 => $first_line_of_address,
450             line_2 => $second_line_of_address,
451             line_3 => $third_line_of_address,
452             city => $city,
453             region => $state_or_province,
454             country => $iso_2_code,
455             postal_code => $postal_or_ZIP_code,
456             latitude => $latitude,
457             longitude => $longitude,
458             tax_region_id => $tax_region_id,
459             };
460            
461             All fields are optional, though without enough to identify an address, your results will
462             be less than satisfying.
463              
464             Country coes are ISO 3166-1 (alpha 2) format, such as 'US'.
465              
466             =cut
467              
468             sub _generate_address_json
469             {
470 0     0     my ( $self, $address, $address_code ) = @_;
471            
472 0           my $address_request = {};
473            
474             # Address code is just an internal identifier. In this module, 1 is destination, 2 is origin.
475 0           $address_request->{'AddressCode'} = $address_code;
476            
477 0           my %nodes =
478             (
479             'line_1' => 'Line1',
480             'line_2' => 'Line2',
481             'line_3' => 'Line3',
482             'city' => 'City',
483             'region' => 'Region',
484             'country' => 'Country',
485             'postal_code' => 'PostalCode',
486             'latitude' => 'Latitude',
487             'longitude' => 'Longitude',
488             'tax_region_id' => 'TaxRegionId',
489             );
490            
491 0           foreach my $node ( keys %nodes )
492             {
493 0 0         if ( defined $address->{ $node } )
494             {
495 0           $address_request->{ $nodes{ $node } } = $address->{ $node };
496             }
497             }
498            
499 0           return $address_request;
500             }
501              
502              
503             =head2 _generate_cart_line_json()
504              
505             Generates a data structure from a cart_line hashref. Cart lines are:
506              
507             my $cart_line = {
508             'line_number' => $number (optional, will be generated if omitted.),
509             'item_code' => $item_code
510             'sku' => $sku, # Use sku OR item_code
511             'tax_code' => $tax_code,
512             'customer_usage_type' => $customer_usage_code
513             'description' => $description,
514             'quantity' => $quantity,
515             'amount' => $amount, # Extended price, ie, price * quantity
516             'discounted' => $is_included_in_discount, # Boolean (True or False)
517             'tax_included' => $is_tax_included, # Boolean (True or False)
518             'ref_1' => $reference_1,
519             'ref_2' => $reference_2,
520             }
521            
522             One of item_code or sku, quantity, and amount are required fields.
523              
524             Customer usage type determines the type of item (sometimes called entity or use code). In some
525             states, different types of items have different tax rates.
526              
527             =cut
528              
529             sub _generate_cart_line_json
530             {
531 0     0     my ( $self, $cart_line, $counter ) = @_;
532            
533 0           my $cart_line_request = {};
534              
535 0   0       $cart_line_request->{'LineNo'} = $cart_line->{'line_number'} // $counter;
536            
537             # By convention, destionation is address 1, origin is address 2, in this module.
538             # It doesn't matter in the slightest, the labels just have to match.
539 0           $cart_line_request->{'DestinationCode'} = 1;
540 0           $cart_line_request->{'OriginCode'} = 2;
541            
542 0           my %nodes =
543             (
544             'item_code' => 'ItemCode',
545             'sku' => 'ItemCode', # Use sku OR item_code
546             'tax_code' => 'TaxCode',
547             'customer_usage_type' => 'CustomerUsageType',
548             'description' => 'Description',
549             'quantity' => 'Qty',
550             'amount' => 'Amount', # Extended price, ie, price * quantity
551             'discounted' => 'Discounted', # Boolean
552             'tax_included' => 'TaxIncluded', # Boolean
553             'ref_1' => 'Ref1',
554             'ref_2' => 'Ref2',
555             );
556            
557 0           foreach my $node ( keys %nodes )
558             {
559 0 0         if ( defined $cart_line->{ $node } )
560             {
561 0           $cart_line_request->{ $nodes{ $node } } = $cart_line->{ $node };
562             }
563             }
564            
565 0           return $cart_line_request;
566             }
567              
568              
569             =head2 _make_request_json()
570              
571             Makes the https request to Avalara, and returns the response json.
572              
573             =cut
574              
575             sub _make_request_json
576             {
577 0     0     my ( $self, $request_json, $resource ) = @_;
578            
579 0   0       $resource //= 'get';
580            
581 0 0         my $request_server = $self->{'is_development'}
582             ? $AVALARA_DEVELOPMENT_REQUEST_SERVER
583             : $AVALARA_REQUEST_SERVER;
584 0           my $request_url = 'https://' . $request_server . '/1.0/tax/' . $resource;
585            
586             # Create a user agent object
587 0           my $user_agent = LWP::UserAgent->new();
588 0           $user_agent->agent( "perl/Business-Tax-Avalara/$VERSION" );
589 0           $user_agent->timeout( $self->{'request_timeout'} );
590            
591             # Create a request
592 0           my $request = HTTP::Request::Common::POST(
593             $request_url,
594             );
595            
596 0           $request->authorization_basic(
597             $self->{'user_name'},
598             $self->{'password'},
599             );
600            
601 0           $request->header( content_type => 'text/json' );
602 0           $request->content( $request_json );
603 0           $request->header( content_length => length( $request_json ) );
604            
605 0 0         if ( $self->{'debug'} )
606             {
607 0           carp( 'Request to Avalara: ', Data::Dump::dump( $request->content() ) );
608             }
609            
610             # Pass request to the user agent and get a response back
611 0           my $response = $user_agent->request( $request );
612            
613 0 0         if ( $self->{'debug'} )
614             {
615 0           carp( 'Response from Avalara: ', Data::Dump::dump( $response->content() ) );
616             }
617              
618             # Check the outcome of the response
619 0 0         if ( $response->is_success() )
620             {
621 0           return $response->content();
622             }
623             else
624             {
625 0           carp $response->status_line();
626 0           carp $request->as_string();
627 0           carp $response->as_string();
628 0           carp "Failed to fetch JSON response: " . $response->status_line() . "\n";
629 0           return $response->content();
630             }
631            
632 0           return;
633             }
634              
635              
636             =head2 _parse_response_json()
637              
638             Converts the returned JSON into a perl hash.
639              
640             =cut
641              
642             sub _parse_response_json
643             {
644 0     0     my ( $self, $response_json ) = @_;
645            
646 0           my $json = JSON::PP->new()->ascii()->pretty()->allow_nonref();
647 0           my $perl = $json->decode( $response_json );
648            
649 0           my $lines = delete $perl->{'TaxLines'};
650 0           foreach my $line ( @$lines )
651             {
652 0           $perl->{'TaxLines'}->{ $line->{'LineNo'} } = $line;
653             }
654            
655 0           return $perl;
656             }
657              
658              
659              
660             =head2 get_memcache()
661              
662             Return the database handle tied to the audit object.
663              
664             my $memcache = $avalara_gateway->get_memcache();
665              
666             =cut
667              
668             sub get_memcache
669             {
670 0     0 1   my ( $self ) = @_;
671              
672 0           return $self->{'memcache'};
673             }
674              
675              
676             =head2 get_cache()
677              
678             Get a value from the cache.
679              
680             my $value = $avalara_gateway->get_cache( key => $key );
681              
682             =cut
683              
684             sub get_cache
685             {
686 0     0 1   my ( $self, %args ) = @_;
687 0           my $key = delete( $args{'key'} );
688 0 0         croak 'Invalid argument(s): ' . join( ', ', keys %args )
689             if scalar( keys %args ) != 0;
690            
691             # Check parameters.
692 0 0 0       croak 'The parameter "key" is mandatory'
693             if !defined( $key ) || $key !~ /\w/;
694            
695 0           my $memcache = $self->get_memcache();
696             return
697 0 0         if !defined( $memcache );
698            
699 0           return $memcache->get( $key );
700             }
701              
702              
703             =head2 set_cache()
704              
705             Set a value into the cache.
706              
707             $avalara_gateway->set_cache(
708             key => $key,
709             value => $value,
710             expire_time => $expire_time,
711             );
712              
713             =cut
714              
715             sub set_cache
716             {
717 0     0 1   my ( $self, %args ) = @_;
718 0           my $key = delete( $args{'key'} );
719 0           my $value = delete( $args{'value'} );
720 0           my $expire_time = delete( $args{'expire_time'} );
721 0 0         croak 'Invalid argument(s): ' . join( ', ', keys %args )
722             if scalar( keys %args ) != 0;
723            
724             # Check parameters.
725 0 0 0       croak 'The parameter "key" is mandatory'
726             if !defined( $key ) || $key !~ /\w/;
727            
728 0           my $memcache = $self->get_memcache();
729             return
730 0 0         if !defined( $memcache );
731            
732 0 0         $memcache->set( $key, $value, $expire_time )
733             || carp 'Failed to set cache with key >' . $key . '<';
734            
735 0           return;
736             }
737              
738              
739             =head1 AUTHOR
740              
741             Kate Kirby, C<< >>.
742              
743              
744             =head1 BUGS
745              
746             Please report any bugs or feature requests to C, or through
747             the web interface at L.
748             I will be notified, and then you'll automatically be notified of progress on
749             your bug as I make changes.
750              
751              
752             =head1 SUPPORT
753              
754             You can find documentation for this module with the perldoc command.
755              
756             perldoc Business::Tax::Avalara
757              
758              
759             You can also look for information at:
760              
761             =over 4
762              
763             =item * RT: CPAN's request tracker
764              
765             L
766              
767             =item * AnnoCPAN: Annotated CPAN documentation
768              
769             L
770              
771             =item * CPAN Ratings
772              
773             L
774              
775             =item * Search CPAN
776              
777             L
778              
779             =back
780              
781              
782             =head1 ACKNOWLEDGEMENTS
783              
784             Thanks to ThinkGeek (L) and its corporate overlords
785             at Geeknet (L), for footing the bill while we eat pizza
786             and write code for them!
787              
788              
789             =head1 COPYRIGHT & LICENSE
790              
791             Copyright 2012 Kate Kirby.
792              
793             This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation.
794              
795             This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
796              
797             You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/
798              
799             =cut
800              
801             1;