package Business::OnlineThirdPartyPayment::PayPal;

use strict;
use base 'Business::OnlineThirdPartyPayment';
use vars qw($VERSION $DEBUG);

use strict;
use LWP;
use JSON;
use Net::PayPal; # for authentication, mostly
use URI;
use Cache::FileCache; # for ID strings

$VERSION = '0.01';

$DEBUG = 2;

sub set_defaults {
  my $self = shift;
  $self->build_subs(qw(order_number result_code error_message error_object
                       cache_root));
}

sub client {
  my $self = shift;
  my %content = $self->content;
  $self->{'client'} ||= 
    Net::PayPal->new($content{'login'}, $content{'password'});
}

sub cache {
  my $self = shift;
  $self->{'cache'} ||=
    Cache::FileCache->new(
      { namespace          => 'PayPal',
        default_expires_in => 3600,
        cache_root         => $self->cache_root
      } );
}

sub submit {
  my $self = shift;
  my %content = $self->content;
  my $action = lc($content{'action'});
  if ( $action eq 'authorization only' ) {
    $self->create_payment;
  } elsif ( $action eq 'post authorization' ) {
    $self->execute_payment;
  }
}

sub rest {
  # a wrapper for the one in Net::PayPal, with better error handling
  my ($self, $path, $request) = @_;
  my $json_request = encode_json($request);
  warn "REQUEST:\n$json_request\n\n" if $DEBUG >= 2;
  my $raw_res =
    $self->client->rest('POST', $path, $json_request, 1);
  # last argument is "dump_responce" [sic]--tells Net::PayPal to dump the 
  # HTTP::Response object instead of returning (part of) the error status
  my $res;
  # deal with certain ambiguities from Data::Dumper
  { my $VAR1;
    eval "$raw_res";
    $res = $VAR1; }
  if ( !defined($res) || !ref($res) || !$res->isa('HTTP::Response') ) {
    die "Nonsense output from Net::PayPal REST call:\n$raw_res\n\n";
  }
  warn "RESPONSE:" . $res->status_line . "\n" . $res->content .  "\n\n"
    if $DEBUG >= 2;

  if ( $res->is_success ) {
    $self->is_success(1);
    return decode_json($res->content);
  } else {
    $self->is_success(0);
    if ( $res->content ) {
      my $response = decode_json($res->content);
      $self->error_object($response);
      my $error = sprintf("%s: %s",
                    $response->{'name'}, $response->{'message'});
      if ( $response->{'details'} ) {
        foreach (@{ $response->{'details'} }) {
          $error .= sprintf("\n%s:\t%s", $_->{'field'}, $_->{'issue'});
        }
      }
      $self->error_message($error);
      return $response;
    } else {
      $self->error_object({});
      $self->error_message($res->status_line);
      return {};
    }
  }
}

sub create_payment {
  my $self = shift;
  my %content = $self->content;
  my $ref = $content{'reference'}
    or die "reference required";
  my $return_url = URI->new($content{'callback_url'})
    or die "callback_url required";
  $return_url->query_form( $return_url->query_form(), 'ref' => $ref );
  my $cancel_url = URI->new($content{'cancel_url'})
    or die "cancel_url required";
  $cancel_url->query_form( $cancel_url->query_form(), 'ref' => $ref );
  
  my $request = 
    {
      intent  => 'sale',
      payer   => {
        payment_method  => 'paypal',
      },
      transactions => [
        {
          amount => {
            total => $content{'amount'},
            currency => ($content{'currency'} || 'USD'),
          },
          description => $content{'description'},
        },
      ],
      redirect_urls => {
        return_url => $return_url->as_string,
        cancel_url => $cancel_url->as_string,
      },
    };

  my $response = $self->rest('/v1/payments/payment', $request);

  if ( $self->is_success ) {
    $self->order_number($response->{'id'});
    $self->cache->set( "REF-$ref" => $response->{'id'} );

    my %links = map { $_->{rel} => $_->{href} } @{ $response->{'links'} };
    $self->popup_url($links{'approval_url'});
    # other links are "self", which is where we just posted,
    # and "execute_url", which we can determine from the payment id
  }
}

sub execute_payment {
  my $self = shift;
  my %content = $self->content;
  # at this point the transaction is already set up
  # (right? the workflow in this is horribly confusing...)
  if ( !$self->authorization ) {
    die "No authorization was received for this payment.\n";
  }
  my $request = { 'payer_id' => $self->authorization };
  my $execute_path = '/v1/payments/payment/' . $self->order_number . '/execute';
  $self->rest($execute_path, $request);
}

sub reference {
  my $self = shift;
  my $data = shift; # hashref of query params included in the callback URL

  $self->authorization($data->{'PayerID'});
  my $ref = $data->{'ref'};
  my $id = $self->cache->get("REF-$ref");
  if (!$id) {
    $self->error_message("Payment reference '$ref' not found.");
    $self->is_success(0);
  }
  $self->order_number($id);
  $ref;
}

1;
__END__

=head1 NAME

Business::OnlineThirdPartyPayment::PayPal

=head1 DESCRIPTION

=head1 AUTHOR

Mark Wells <mark@freeside.biz>

=head1 SEE ALSO

perl(1). L<Business::OnlineThirdPartyPayment>.

=cut

