The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Business::CPI::Gateway::PagSeguro;
# ABSTRACT: Business::CPI's PagSeguro driver

use Moo;
use XML::LibXML;
use Carp;
use LWP::Simple ();
use URI;
use URI::QueryParam;
use DateTime;
use Locale::Country ();
use Data::Dumper;

extends 'Business::CPI::Gateway::Base';

our $VERSION = '0.66'; # VERSION

has '+checkout_url' => (
    default => sub { 'https://pagseguro.uol.com.br/v2/checkout/payment.html' },
);

has '+currency' => (
    default => sub { 'BRL' },
);

has base_url => (
    is => 'ro',
    default => sub { 'https://ws.pagseguro.uol.com.br/v2' },
);

has token => (
    is  => 'ro',
);

sub get_notifications_url {
    my ($self, $code) = @_;

    return $self->_build_uri("/transactions/notifications/$code");
}

sub get_transaction_details_url {
    my ($self, $code) = @_;

    return $self->_build_uri("/transactions/$code");
}

sub get_transaction_query_url {
    my ($self, $info) = @_;

    $info ||= {};

    my $final_date   = $info->{final_date}   || DateTime->now(time_zone => 'local'); # XXX: really local?
    my $initial_date = $info->{initial_date} || $final_date->clone->subtract(days => 30);

    my $new_info = {
        initialDate    => $initial_date->strftime('%Y-%m-%dT%H:%M'),
        finalDate      => $final_date->strftime('%Y-%m-%dT%H:%M'),
        page           => $info->{page} || 1,
        maxPageResults => $info->{rows} || 1000,
    };

    return $self->_build_uri('/transactions', $new_info);
}

sub query_transactions { goto \&get_and_parse_transactions }

sub get_and_parse_notification {
    my ($self, $code) = @_;

    my $xml = $self->_load_xml_from_url(
        $self->get_notifications_url($code)
    );

    if ($self->log->is_debug) {
        $self->log->debug("The notification we received was:\n" . Dumper($xml));
    }

    return $self->_parse_transaction($xml);
}

sub notify {
    my ($self, $req) = @_;

    if ($req->params->{notificationType} eq 'transaction') {
        my $code = $req->params->{notificationCode};

        $self->log->info("Received notification for $code");

        my $result = $self->get_and_parse_notification( $code );

        if ($self->log->is_debug) {
            $self->log->debug("The notification we're returning is " . Dumper($result));
        }
    }
}

sub get_and_parse_transactions {
    my ($self, $info) = @_;

    my $xml = $self->_load_xml_from_url(
        $self->get_transaction_query_url( $info )
    );

    my @transactions = $xml->getChildrenByTagName('transactions')->get_node(1)->getChildrenByTagName('transaction');

    return {
        current_page         => $xml->getChildrenByTagName('currentPage')->string_value,
        results_in_this_page => $xml->getChildrenByTagName('resultsInThisPage')->string_value,
        total_pages          => $xml->getChildrenByTagName('totalPages')->string_value,
        transactions         => [
            map { $self->get_transaction_details( $_ ) }
            map { $_->getChildrenByTagName('code')->string_value } @transactions
        ],
    };
}

sub get_transaction_details {
    my ($self, $code) = @_;

    my $xml = $self->_load_xml_from_url(
        $self->get_transaction_details_url( $code )
    );

    my $result = $self->_parse_transaction($xml);
    $result->{buyer_email} = $xml->getChildrenByTagName('sender')->get_node(1)->getChildrenByTagName('email')->string_value;

    return $result;
}

sub _parse_transaction {
    my ($self, $xml) = @_;

    my $date   = $xml->getChildrenByTagName('date')->string_value;
    my $ref    = $xml->getChildrenByTagName('reference')->string_value;
    my $status = $xml->getChildrenByTagName('status')->string_value;
    my $amount = $xml->getChildrenByTagName('grossAmount')->string_value;
    my $net    = $xml->getChildrenByTagName('netAmount')->string_value;
    my $fee    = $xml->getChildrenByTagName('feeAmount')->string_value;
    my $code   = $xml->getChildrenByTagName('code')->string_value;
    my $payer  = $xml->getChildrenByTagName('sender')->get_node(1)->getChildrenByTagName('name')->string_value;

    return {
        payment_id             => $ref,
        gateway_transaction_id => $code,
        status                 => $self->_interpret_status($status),
        amount                 => $amount,
        date                   => $date,
        net_amount             => $net,
        fee                    => $fee,
        exchange_rate          => 0,
        payer => {
            name => $payer,
        },
    };
}

sub _load_xml_from_url {
    my ($self, $url) = @_;

    return XML::LibXML->load_xml(
        string => LWP::Simple::get( $url )
    )->firstChild();
}

sub _build_uri {
    my ($self, $path, $info) = @_;

    $info ||= {};

    $info->{email} = $self->receiver_email;
    $info->{token} = $self->token;

    my $uri = URI->new($self->base_url . $path);

    while (my ($k, $v) = each %$info) {
        $uri->query_param($k, $v);
    }

    return $uri->as_string;
}

sub _interpret_status {
    my ($self, $status) = @_;

    $status = int($status || 0);

    # 1: aguardando pagamento
    # 2: em análise
    # 3: paga
    # 4: disponível
    # 5: em disputa
    # 6: devolvida
    # 7: cancelada

    my @status_codes = ('unknown');
    @status_codes[1,2,5] = ('processing') x 3;
    @status_codes[3,4]   = ('completed') x 2;
    $status_codes[6]     = 'refunded';
    $status_codes[7]     = 'failed';

    if ($status > 7) {
        return 'unknown';
    }

    return $status_codes[$status];
}

sub get_hidden_inputs {
    my ($self, $info) = @_;

    my $buyer = $info->{buyer};
    my $cart  = $info->{cart};

    my @hidden_inputs = (
        receiverEmail => $self->receiver_email,
        currency      => $self->currency,
        encoding      => $self->form_encoding,
        reference     => $info->{payment_id},
        senderName    => $buyer->name,
        senderEmail   => $buyer->email,
    );

    my %buyer_extra = (
        address_complement => 'shippingAddressComplement',
        address_district   => 'shippingAddressDistrict',
        address_street     => 'shippingAddressStreet',
        address_number     => 'shippingAddressNumber',
        address_city       => 'shippingAddressCity',
        address_state      => 'shippingAddressState',
        address_country    => 'shippingAddressCountry',
        address_zip_code   => 'shippingAddressPostalCode',
    );

    for (keys %buyer_extra) {
        if (my $value = $buyer->$_) {
            if ($_ eq 'shippingAddressCountry') {
                $value = uc(
                    Locale::Country::country_code2code(
                        $value, 'alpha-2', 'alpha-3'
                    )
                );
            }
            push @hidden_inputs, ( $buyer_extra{$_} => $value );
        }
    }

    my $extra_amount = 0;

    if (my $disc = $cart->discount) {
        $extra_amount -= $disc;
    }

    if (my $handl = $cart->handling) {
        $extra_amount += $handl;
    }

    if (my $tax = $cart->tax) {
        $extra_amount += $tax;
    }

    if ($extra_amount) {
        $extra_amount = sprintf( "%.2f", $extra_amount );
        push @hidden_inputs, ( extraAmount => $extra_amount );
    }

    my $i = 1;

    foreach my $item (@{ $info->{items} }) {
        push @hidden_inputs,
          (
            "itemId$i"          => $item->id,
            "itemDescription$i" => $item->description,
            "itemAmount$i"      => $item->price,
            "itemQuantity$i"    => $item->quantity,
          );

        if (my $weight = $item->weight) {
            push @hidden_inputs, ( "itemWeight$i" => $weight * 1000 ); # show in grams
        }

        if (my $ship = $item->shipping) {
            push @hidden_inputs, ( "itemShippingCost$i" => $ship );
        }

        $i++;
    }

    return @hidden_inputs;
}

1;

__END__

=pod

=encoding utf-8

=head1 NAME

Business::CPI::Gateway::PagSeguro - Business::CPI's PagSeguro driver

=head1 VERSION

version 0.66

=head1 ATTRIBUTES

=head2 token

The token provided by PagSeguro

=head2 base_url

The url for PagSeguro API. Not to be confused with the checkout url, this is
just for the API.

=head1 METHODS

=head2 get_notifications_url

Reader for the notifications URL in PagSeguro's API. This uses the base_url
attribute.

=head2 get_transaction_details_url

Reader for the transaction details URL in PagSeguro's API. This uses the
base_url attribute.

=head2 get_transaction_query_url

Reader for the transaction query URL in PagSeguro's API. This uses the base_url
attribute.

=head2 get_and_parse_notification

Gets the url from L</get_notifications_url>, and loads the XML from there.
Returns a parsed standard Business::CPI hash.

=head2 get_and_parse_transactions

=head2 get_transaction_details

=head2 query_transactions

Alias for L</get_and_parse_transactions> to maintain compatibility with other
Business::CPI modules.

=head2 notify

=head2 get_hidden_inputs

=head1 SPONSORED BY

Aware - L<http://www.aware.com.br>

=head1 SEE ALSO

L<Business::CPI::Gateway::Base>

=head1 AUTHOR

André Walker <andre@andrewalker.net>

=head1 CONTRIBUTOR

Renato CRON <rentocron@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2013 by André Walker.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut