Group
Extension

WWW-Billomat/lib/WWW/Billomat.pm

package WWW::Billomat;

use 5.010;

use strict;
use warnings;

use Moose;

use URI;
use REST::Client;
use Encode;

use WWW::Billomat::Client;
use WWW::Billomat::Invoice;
use WWW::Billomat::Invoice::Item;

use JSON;

=head1 NAME

WWW::Billomat - API access to Billomat services

=head1 VERSION

Version 0.01

=cut

our $VERSION = '0.01';

=head1 SYNOPSIS

    use WWW::Billomat;

    my $billomat = WWW::Billomat->new(
        billomat_id => 'foo',
        api_key => 'blahblah12345678',
    );
    
    my $client = $billomat->get_client(123);
    my $invoice = WWW::Billomat::Invoice->new(
        client_id => $client->id,
        number => 456,
        discount_rate => 50,
    );
    if(not $billomat->create_invoice($invoice)) {
        die "ERROR creating invoice: " . $billomat->response_content();
    }

=cut

has billomat_id => (
    is => 'rw',
    isa => 'Str',
    required => 1,
);

has api_key => (
    is => 'rw',
    isa => 'Str',
    required => 1,
);

has rest_client => (
    is => 'rw',
    isa => 'REST::Client',
    lazy_build => 1,
    handles => {
        response_code => 'responseCode',
        response_content => 'responseContent',
        response_headers => 'responseHeaders',
    },
);

has request_body => (
    is => 'rw',
    isa => 'Str',
);

has base_url => (
    is => 'rw',
    isa => 'URI',
    lazy_build => 1,
);

=head1 DESCRIPTION

This module is an interface to the Billomat API, "the simple online
service for quoting, billing and more".

For more information:

=over 4

=item http://www.billomat.com/en/

=item http://www.billomat.com/en/api

=back

Note the implementation is B<partial>. The currently implement feature 
set is:

=over 4

=item * list, create, edit, delete clients

=item * set client properties

=item * list, create, edit, delete invoices

=item * list, create invoice items

=item * complete invoices and get PDF output

=back

=head1 SUBROUTINES/METHODS

B<NOTE>: all methods return either undef or false in case of failure.
To investigate error conditions, three methods are delegated to the
underlying L<REST::Client> object:

=over 4

=item * response_code

=item * response_content

=item * response_headers

=back

Example:

    if(not $billomat->create_invoice($invoice)) {
        die "ERROR creating invoice: " . $billomat->response_content();
    }

=head2 get_clients( [PARAMS] )

Returns an array of L<WWW::Billomat::Client> objects, 
or undef on failure.

See L<WWW::Billomat::Client> for search parameters.

Example:

    my @clients = $billomat->get_clients( name => 'gmbh' );

=cut

sub get_clients {
    my($self, %params) = @_;
    return $self->_search_objects(
        'WWW::Billomat::Client', %params
    );
}

=head2 get_client( ID )

Returns the L<WWW::Billomat::Client> object with the given ID,
or undef on failure.

Example:

    my $client = $billomat->get_client( 123 );

=cut

sub get_client {
    my($self, $id) = @_;
    return $self->_get_object(
        'WWW::Billomat::Client', $id, 
    );
}

=head2 create_client( CLIENT )

Creates a new client. Expects a L<WWW::Billomat::Client> object
as argument.

Returns the created object, or undef on failure.

Example:

    my $client = WWW::Billomat::Client->new(
        name => 'Musterfirma',
        salutation => 'Herr',
        first_name => 'Max',
        last_name => 'Muster',
        # etc.
    );
    if( $billomat->create_client( $client ) ) {
        say "Client created";
    }

=cut

sub create_client {
    my($self, $client) = @_;
    return $self->_create_object($client);
}

=head2 delete_client( CLIENT )

Deletes a client. CLIENT can be either a L<WWW::Billomat::Client>
object, or its ID.

Returns true on success, false on failure.

Example:

    $billomat->delete_client( $client );
    $billomat->delete_client( 123 );

=cut

sub delete_client {
    my($self, $client) = @_;
    return $self->_delete_object(
        'WWW::Billomat::Client', $client
    );
}

=head2 edit_client( CLIENT )

Updates a client with the current CLIENT (L<WWW::Billomat::Client>)
object properties.

Returns true on success, false on failure.

Example:

    my $client = $billomat->get_client( name => 'Foo' );
    $client->name( 'FooBar' );
    $billomat->edit_client($client);

=cut

sub edit_client {
    my($self, $client) = @_;
    return $self->_edit_object($client);
}

=head2 set_client_property( CLIENT, ID, VALUE )

Sets a custom property for a client.

Returns true on success, false on failure.

Example:

    $billomat->set_client_property( $client, 123 => 'foo' );

=cut

sub set_client_property {
    my($self, $client, $id, $value) = @_;
    if(ref $client) {
        $client = $client->id;
    }
    my $body = sprintf(
        '<?xml version="1.0" encoding="UTF-8" ?>'."\n".
        "<client-property-value>".
        "<client_id>%d</client_id>".
        "<client_property_id>%d</client_property_id>".
        "<value>%s</value>".
        "</client-property-value>",
        $client,
        $id,
        $value
    );
    $body = Encode::encode('ASCII', $body, Encode::FB_XMLCREF);
    $self->request_body($body);
    # $body = Encode::encode('utf-8', $body);
    my $response = $self->rest_client->POST(
        "client-property-values",
        $body, 
        { 'Content-Type' => 'application/xml; charset=utf-8' }
    );
    return $response->responseCode() == 201;
}

=head2 get_invoices( [PARAMS] )

Returns an array of L<WWW::Billomat::Invoice> objects, 
or undef on failure.

See L<WWW::Billomat::Invoice> for search parameters.

Example:

    my @invoices = $billomat->get_invoices( client_id => 1 );
    
=cut

sub get_invoices {
    my($self, %params) = @_;
    return $self->_search_objects(
        'WWW::Billomat::Invoice', %params
    );
}

=head2 get_invoice( ID )

Returns the L<WWW::Billomat::Invoice> object with the given ID,
or undef on failure.

Example:

    my $invoice = $billomat->get_invoice( 123 );

=cut

sub get_invoice {
    my($self, $id) = @_;
    return $self->_get_object(
        'WWW::Billomat::Invoice', $id
    );
}

=head2 create_invoice( INVOICE )

Creates a new invoice. Expects a L<WWW::Billomat::Invoice> object
as argument.

Returns the created object, or undef on failure.

Example:

    my $invoice = WWW::Billomat::Invoice->new(
        client_id => 123,
        # etc.
    );
    if( $billomat->create_invoice( $invoice ) ) {
        say "Invoice created";
    }

=cut

sub create_invoice {
    my($self, $invoice) = @_;
    return $self->_create_object($invoice);
}

=head2 delete_invoice( INVOICE )

Deletes an invoice. INVOICE can be either a L<WWW::Billomat::Invoice>
object, or its ID.

Returns true on success, false on failure.

Example:

    $billomat->delete_invoice( $invoice );
    $billomat->delete_invoice( 123 );

=cut

sub delete_invoice {
    my($self, $invoice) = @_;
    return $self->_delete_object(
        'WWW::Billomat::Invoice', $invoice,
    );
}

=head2 edit_invoice( INVOICE )

Updates an invoice with the current INVOICE (L<WWW::Billomat::Invoice>)
object properties.

Returns true on success, false on failure.

Example:

    my $invoice = $billomat->get_invoice( 123 );
    $invoice->due_date( 'yesterday' );
    $billomat->edit_invoice($invoice);

=cut

sub edit_invoice {
    my($self, $client) = @_;
    return $self->_edit_object($client);
}

=head2 complete_invoice( INVOICE, TEMPLATE_ID )

Closes an invoice and generates a PDF for it with the given
TEMPLATE_ID.
INVOICE can be either a L<WWW::Billomat::Invoice> object, or its ID.

Returns true on success, false on failure.

Example:

    die unless $billomat->complete_invoice( $invoice, 123 );

=cut

sub complete_invoice {
    my($self, $invoice, $template_id) = @_;
    my $id = $invoice;
    if(ref $invoice) {
        $id = $invoice->id;
    }    
    my $body = sprintf(
        '<?xml version="1.0" encoding="UTF-8" ?>'."\n".
        "<complete><template_id>%d</template_id></complete>",
        $template_id,
    );
    $body = Encode::encode('ASCII', $body, Encode::FB_XMLCREF);
    $self->request_body($body);
    # $body = Encode::encode('utf-8', $body);
    my $response = $self->rest_client->PUT(
        sprintf(
            "%s/%d/complete",
            WWW::Billomat::Invoice->api_resource, 
            $id,
        ),
        $body, 
        { 'Content-Type' => 'application/xml; charset=utf-8' }
    );
    return $response->responseCode() == 200;
}

=head2 get_invoice_pdf( INVOICE )  

Returns the PDF for an invoice 
(note that you must call L</complete_invoice> first). 
INVOICE can be either a L<WWW::Billomat::Invoice> object, or its ID.

Returns the (binary) PDF data, or undef on failure.

Example:

    if(my $pdf = $billomat->get_invoice_pdf($invoice)) {
        open(my $output, '>', 'foo.pdf');
        binmode($output);
        print $output $pdf;
        close($output);
    }

=cut

sub get_invoice_pdf {
    my($self, $invoice) = @_;

    my $id = $invoice;
    if(ref $invoice) {
        $id = $invoice->id;
    }
    
    my $api = URI->new(sprintf(
        "%s/%d/pdf",
        WWW::Billomat::Invoice->api_resource, 
        $id,
    ));
    $api->query_form( format => 'pdf' );
    my $response = $self->rest_client->GET($api->as_string);
    if($response->responseCode() == 200) {
        return $response->responseContent();
    }
}

=head2 get_invoice_items( INVOICE )

Returns an array of L<WWW::Billomat::Invoice::Item> objects,
or undef on failure.

INVOICE can be either a L<WWW::Billomat::Invoice>
object, or its ID.

Example:

    my @items = $billomat->get_invoice_items( $invoice );

=cut

sub get_invoice_items {
    my($self, $invoice) = @_;
    my $id = $invoice;
    if(ref $invoice) {
        $id = $invoice->id;
    }
    return $self->_search_objects(
        'WWW::Billomat::Invoice::Item', invoice_id => $id,
    );
}

=head2 create_invoice_item( ITEM )

Creates a new invoice item. Expects a L<WWW::Billomat::Invoice::Item> 
object as argument.

Returns the created object, or undef on failure.

Example:

    my $item = WWW::Billomat::Invoice::Item->new(
        invoice_id => 123,
        title => 'Cookies',
        quantity => 1_000,
        unit_price => 0.50,
        # etc.
    );
    if( $billomat->create_invoice_item( $item ) ) {
        say "Invoice item created";
    }

=cut

sub create_invoice_item {
    my($self, $item) = @_;
    return $self->_create_object($item);
}

# private methods

sub _build_rest_client {
    my($self) = @_;

    my $client = REST::Client->new(
        host => $self->base_url->as_string,
    );
    $client->addHeader(
        'X-BillomatApiKey', $self->api_key,
    );
    return $client;
}

sub _build_base_url {
    my($self) = @_;

    my $uri = URI->new(
        sprintf("https://%s.billomat.net", $self->billomat_id)
    );
    $uri->path('/api');
    return $uri;
}

sub _populate_from_xml {
    my($self, $object, $xml) = @_;
    foreach my $attribute ($object->meta->get_all_attributes) {
        my $attr = $attribute->name;
        my $value = $xml->findvalue($attr);
        if($value) {
            $object->$attr($xml->findvalue($attr));
        }
    }
    return $object;
}

sub _search_objects {
    my($self, $class, %params) = @_;
    my %query;
    foreach my $param (@{ $class->search_params }) {
        if(exists $params{$param}) {
            $query{$param} = $params{$param};
        }
    }
    my $api = URI->new($class->api_resource);
    $api->query_form(%query);
    return $self->_get_objects(
        $api->as_string, $class
    );
}

sub _get_objects {
    my($self, $api, $class) = @_;
    
    my $response = $self->rest_client->GET($api);
    if($response->responseCode() == 200) {
        my $dom = $response->responseXpath();
        my $xpath = sprintf('//%s/%s',
            $class->api_container_tag,
            $class->api_item_tag,
        );
        my @nodes = $dom->findnodes($xpath);
        my @objects;
        foreach my $node (@nodes) {
            push(@objects, $self->_populate_from_xml(
                $class->new, $node
            ));
        }
        return @objects;
    } else {
        return undef;
    }
}

sub _get_object {
    my($self, $class, $id) = @_;
    
    my $api = sprintf('%s/%s',
        $class->api_resource,
        $id,
    );	
    my $response = $self->rest_client->GET($api);
    if($response->responseCode() == 200) {
        return $self->_get_object_from_response(
            $response, $class
        );
    } else {
        return undef;
    }
}

sub _get_object_from_response {
    my($self, $response, $class) = @_;
    my $dom = $response->responseXpath();
    my $xpath = $class->api_item_tag;
    my @nodes = $dom->findnodes($xpath);
    if(defined $nodes[0]) {
        return $self->_populate_from_xml(
            $class->new, $nodes[0]
        );
    }
    return undef;
}

sub _create_object {
    my($self, $object) = @_;
    my $body = $self->_to_json($object);
    # warn "BODY=$body\n";
    # $body = Encode::encode('ASCII', $body, Encode::FB_XMLCREF);
    $self->request_body($body);
    my $response = $self->rest_client->POST(
        $object->api_resource, $body, {
            # 'Content-Type' => 'application/xml; charset=utf-8'
            'Content-Type' => 'application/json'
        }
    );
    if($response->responseCode() == 201) {
        return $self->_get_object_from_response(
            $response, $object->meta->name,
        );
    } else {
        return undef;
    }
}

sub _delete_object {
    my($self, $class, $object) = @_;
    if(ref $object) {
        $object = $object->id;
    }
    my $response = $self->rest_client->DELETE(
        $class->api_resource . '/' . $object
    );
    return $response->responseCode() == 200;
}

sub _edit_object {
    my($self, $object) = @_;
    my $body = $self->_to_json($object);
    $self->request_body($body);
    my $response = $self->rest_client->PUT(
        $object->api_resource . '/' . $object->id,
        $body, {
            # 'Content-Type' => 'application/xml; charset=utf-8',
            'Content-Type' => 'application/json',
        }
    );
    return $response->responseCode() == 200;
}

sub _to_xml {
    my($self, $object) = @_;
    my $node_name = $object->api_item_tag;
    my $xml = '<?xml version="1.0" encoding="UTF-8" ?>'."\n";    
    $xml .= "<$node_name>\n";
    foreach my $attribute ($object->meta->get_all_attributes) {
        my $attr = $attribute->name;
        if(defined $object->$attr()) {
            $xml .= sprintf("\t<%s>%s</%s>\n",
                # $attr, Encode::encode_utf8 $object->$attr(), $attr
                $attr, $object->$attr(), $attr
            );
        }
    }
    $xml .= "</$node_name>\n";
    return $xml;
}

sub _to_json {
    my($self, $object) = @_;
    my $node_name = $object->api_item_tag;
    my $json = { $object->api_item_tag => {} };
    foreach my $attribute ($object->meta->get_all_attributes) {
        my $attr = $attribute->name;
        if(defined $object->$attr()) {
            $json->{$object->api_item_tag}->{$attr} = $object->$attr();
        }
    }
    return encode_json $json;
}

=head1 AUTHOR

Aldo Calpini, C<< <dada at perl.it> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-www-billomat at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=WWW-Billomat>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc WWW::Billomat

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=WWW-Billomat>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/WWW-Billomat>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/WWW-Billomat>

=item * Search CPAN

L<http://search.cpan.org/dist/WWW-Billomat/>

=back

=head1 SOURCE

The development version is on github at
L<http://github.com/dada/WWW-Billomat>
and may be cloned from
C<git://github.com/dada/WWW-Billomat.git>.

=head1 LICENSE AND COPYRIGHT

Copyright 2013 Aldo Calpini.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut

__PACKAGE__->meta->make_immutable;

1; # End of WWW::Billomat


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.