The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package WWW::Purolator::TrackingInfo;

use warnings;
use strict;

our $VERSION = '1.0105';
use 5.006;
use LWP::UserAgent;
use JSON::PP qw//;
use base 'Class::Accessor::Grouped';
__PACKAGE__->mk_group_accessors( simple => qw/
    error
    _ua
/);

sub new {
    my $class = shift;
    my %options = @_;
    my $self = bless {}, $class;

    $self->_ua(
        LWP::UserAgent->new(
            agent => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:26.0) '
                . 'Gecko/20100101 Firefox/26.0', timeout => 30 ),
    );

    return $self;
}

sub track {
    my ( $self, $pin ) = @_;

    $self->error(undef);

    my $res = $self->_ua->get(
        'http://www.purolator.com/en/ship-track/tracking-details.page?pin='
            . $pin,
    );

    $res->is_success
        or return $self->_set_error('Network error: ' . $res->status_line);

    return $self->_parse( $pin, $res->decoded_content );
}

sub _parse {
    my ( $self, $pin, $content ) = @_;

    my %info = ( pin => $pin );

    $content =~ /Our online tracking system is currently unavailable/
        and return $self->_set_error(
            'Tracking system is currently unavailable'
        );

    my ( $z ) = $content =~ m{var jsHistoryTable = (\[.+?\]);}s;

    my $history_table = eval {
        JSON::PP->new->allow_singlequote->decode( $z );
    };
    $@
    and return $self->_set_error(
            'Error parsing Purolator\'s data: ' . $@
            . '. Perhaps you supplied an invalid PIN/tracking number?'
        );

    for ( @$history_table ) {
        my %data;
        @data{qw/scan_date  scan_time  location  comment/} = @$_;
        $_ =  \%data;
    }

    $info{history} = $history_table;


    my ( $status_code ) = $content =~ m{
        var \s+ detailsData \s+ =
        \s+
        \{
            \s+ "trackingNumber" .+?
            "status": \s+ '([^']+)'
        }six;

    my %possible_statuses = (
        InTransit       => 'in transit',
        Pickup          => 'package picked up',
        DataReceived    => 'shipping label created',
        Attention       => 'attention',
        Delivered       => 'delivered',
    );

    $info{status}
    = $possible_statuses{ $status_code } || 'unknown status code';

    return \%info;
}

sub _set_error {
    my ( $self, $error ) = @_;

    $self->error( $error );

    return;
}

q|
I'd like to make the world a better place,
but they won't give me the source code.
|;
__END__

=head1 NAME

WWW::Purolator::TrackingInfo - access Purolator's tracking information

=head1 SYNOPSIS

    use strict;
    use warnings;
    use WWW::Purolator::TrackingInfo;

    my $t = WWW::Purolator::TrackingInfo->new;

    my $info = $t->track('320698592781')
        or die "Error: " . $t->error;

    if ( $info->{status} eq 'delivered' ) {
        print "The package has been delivered! YEY!\n";
    }
    else {
        print "Package's latest update is: $info->{history}[0]{comment}\n";
    }

=head1 DESCRIPTION

The module accesses L<www.purolator.com|http://www.purolator.com>
and gets tracking information for the package, using the provided
PIN (tracking number, e.g. 320698592781).

=head1 CONSTRUCTOR

    my $t = WWW::Purolator::TrackingInfo->new;

Creates and returns a new C<WWW::Purolator::TrackingInfo> object.
Does not take any arguments.

=head1 METHODS/ACCESSORS

=head2 C<track>

    my $info = $t->track('320698592781')
        or die $t->error;

Instructs the object to obtain tracking information from Purolator using
a PIN. B<Takes> one mandatory argument: Purolator's PIN for
the package (or "tracking number"; years after dealing with Purolator,
I'm still unclear on their terminology).
B<On failure> returns C<undef> or an empty list, depending on the
context, and the reason for failure will be available
via C<< ->error >> method.
B<On success> returns a hashref with the following keys/values
(sample abridged data):

    {
        'status' => 'delivered'
        'pin' => '320698611680',
        'history' => [
            {
                'comment' => 'Shipping label created with reference(s): 2509543',
                'location' => 'Purolator',
                'scan_time' => '11:09:00',
                'scan_date' => '2014-01-16'
            }
        ],
    };

=head3 C<status>

    'in transit',
    'package picked up',
    'shipping label created',
    'attention',
    'delivered',

The C<status> value will be one of the above values, with a possible
additional one C<'unknown status code'>, though the unknown code
likely would mean this module is broken. The values are self-explanatory,
with exception of C<'attention'>, which means some unforseen event
has happened with the delivery and the package status requires attention.

=head3 C<pin>

    'pin' => '320698611680',

This is the PIN/tracking number that was used to call
C<< ->track >> with.

=head3 C<history>

    'history' => [
        {
            'comment' => 'Shipment delivered to MARY at: RECEPTION',
            'location' => 'Saskatoon, SK',
            'scan_time' => '10:44:00',
            'scan_date' => '2014-01-17'
        },
        {
            'comment' => 'On vehicle for delivery',
            'location' => 'Saskatoon, SK',
            'scan_time' => '09:57:00',
            'scan_date' => '2014-01-17'
        },
        {
            'comment' => 'Arrived at sort facility',
            'location' => 'Saskatoon, SK',
            'scan_time' => '06:57:00',
            'scan_date' => '2014-01-17'
        },
        {
            'comment' => 'Picked up by Purolator at  CALGARY AB ',
            'location' => 'Calgary, AB',
            'scan_time' => '15:23:00',
            'scan_date' => '2014-01-16'
        },
        {
            'comment' => 'Shipping label created with reference(s): 2509543',
            'location' => 'Purolator',
            'scan_time' => '11:09:00',
            'scan_date' => '2014-01-16'
        }
    ];

The value is an arrayref of hashrefs. Each hashref specifies a line
in package's tracking history, most recent first. Each
hashref contains four keys:

=head3 C<scan_date>

    'scan_date' => '2014-01-17'

The date of this particular update.

=head3 C<scan_time>

    'scan_time' => '15:23:00',

The time of this particular update.

=head3 C<location>

    'location' => 'Calgary, AB',

Location of where the update happened.

=head3 C<comment>

    'comment' => 'Shipping label created with reference(s): 2509543',

The comment/description of the update.

=head2 C<error>

    $t->track('320698592781')
        or die $t->error;

Takes no arguments. Returns a human readable reason for why
C<< ->track >> method failed.

=head1 AUTHOR

'Zoffix, C<< <'zoffix at cpan.org'> >>
(L<http://haslayout.net/>, L<http://zoffix.com/>, L<http://zofdesign.com/>)

=head1 BUGS

Please report any bugs or feature requests to C<bug-www-purolator-trackinginfo at rt.cpan.org>, or through
the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=WWW-Purolator-TrackingInfo>.  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::Purolator::TrackingInfo

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker
 LWP::UserAgent->new( agent => 'Opera 9.5', timeout => 30 )
L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=WWW-Purolator-TrackingInfo>

=item * AnnoCPAN: Annotated CPAN documentation

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

=item * CPAN Ratings

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

=item * Search CPAN

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

=back

=head1 COPYRIGHT & LICENSE

Copyright 2009 'Zoffix, all rights reserved.

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


=cut