Compare commits

...

16 commits

Author SHA1 Message Date
Bradley M. Kuhn
2beee595e2 Restore files generated by Dist::Zilla for ease of building.
Makefile.PL was originally in this repository but removed by Olaf at
one point in favor of the Perl Dist::Zilla system.  However, to ease
instructions for Conservancy folks who want to use this library to
run our legacy PayPal importer, I've restored the files in this soft
fork of upstream.  This way, Dist::Zilla need not be installed to
build these libraries.
2020-07-20 17:33:07 +00:00
Bradley M. Kuhn
20967ab871 Merge branch 'recurring-payments-profile-details-support' 2015-01-03 16:57:18 -05:00
Bradley M. Kuhn
53355d9e4c These variables are deprecated in these files.
These strings appear to be removed from the other files that I
originally adapted this from, which was from an older version of this
API.
2015-01-03 16:56:51 -05:00
Bradley M. Kuhn
336e401ac0 GetRecurringPaymentsProfileDetails API
This uses PayPal's SOAP API in a similar manner to other parts of this
API.

I adapted this about a year ago to provide these functions, basing it on
code from other parts of the API.
2015-01-03 16:56:51 -05:00
Bradley M. Kuhn
fd2d2e73f2 Merge branch 'upstream-master' 2015-01-03 16:55:34 -05:00
Olaf Alders
267e814e70 Formatting and naming changes in t/SubscriptionPayments.t 2015-01-02 23:05:36 -05:00
Olaf Alders
8b755e0e66 Merge pull request #3 from bkuhn/profile-id-and-subscription-search-tests
Support ProfileID in TransactionSearch,  and add subscription search tests that use it.
2015-01-02 22:48:03 -05:00
Bradley M. Kuhn
4a0e47a78a Merge branch 'correct-options-output'
Conflicts:
	.gitignore
2015-01-02 13:35:00 -05:00
Bradley M. Kuhn
aef0d8762c Add my copyright notice.
While I'm not a fan of file-by-file copyright inventory, as is used
here, I have added my copyright notice to this file alongside the
existing one, since I've made copyrightable changes to this file.
2015-01-02 13:17:08 -05:00
Bradley M. Kuhn
5113ced7e3 Properly extract Options from PaymentItemInfo
The Options fields found in
/PaymentTransactionDetails/PaymentItemInfo/PaymentItem/Options have
never been extracted in the proper way.  In fact, I'd be surprised if
anyone found the previous method of extraction useful at all.

I discovered this bug when I needed to lookup the Options selected
during payment, and found they weren't returned by the API.

Specifically, the value inside the SOAP tags, which is what the API
previously returned, are simply not useful: they are typically empty.
The interesting data is stored in the attributes of 'name' and 'value'.

Note, BTW, that each PaymentItem can have its own set of Options.  This
code added herein properly extracts the Options data for each
PaymentItem, and places it both in the Options field for that
PaymentItem, and in the array in the PII_Options "convenience" field.

As can be seen in the accompanying changes to t/OptionsFields.t, the
return values of the Options data is now a hash rather than a list.  I
believe this API-user-visible change is appropriate since a hash is
ultimately the natural representation of this data, and since having the
Options as an array of empty strings wasn't useful, anyway.

Nevertheless, if existing user code of this API relies on Options being
an array of empty strings (which I suppose could have been usefully used
in scalar context to get the count of options in the record), this
change theoretically creates a user-visible change in the API.  I can't
imagine anyone found the previous return value useful in the past
anyway, so I'd be surprised if anyone relies on this mis-feature.  If
they did, they can simply change their code to:
       scalar keys ...{Options}
instead of:
       scalar @...{Options}

However, since this is technically an API change, I've updated the
Changes file and the module documentation to mention it.
2015-01-02 13:15:46 -05:00
Bradley M. Kuhn
dd503a3080 Remove unnecessary use floating around in code.
@oalders noted in patch review on a pull request:
> Looks like this might be left over from debugging?

And he was indeed correct. ;)

I've removed the use.  Data::Dumper isn't used by this test.
2015-01-02 11:44:02 -05:00
Bradley M. Kuhn
7aeeb15fcf Consolidate use's to top of file.
@oalders suggests in a comment on my pull request:
> Could we move this use up to to the top of the file with the others?

So I've done so here.
2015-01-02 11:43:21 -05:00
Bradley M. Kuhn
9045d3d511 Switch to use autodie to simplify code.
@oalders suggests in a pull request comment:
> If we use autodie then we won't need to check for success on open().

I've made this change to accommodate that suggestion.
2015-01-02 11:42:10 -05:00
Bradley M. Kuhn
c5f62dbdb1 Beginnings of tests for subscription payments.
These tests verify a TransactionSearch for ProfileID, and also serve as
an example of the method to find subscriptions using that method.

I've also added to the test a creation of an HTML form that can be used
to create a transaction that will allow the test to pass.  This is a
hack, not unlike the hacks existing in the tests now that ask for
pasting of a transaction ID.  Long term, if someone wants to do the
work, using WWW:Mechanize or some other method for full automation of
these tests might be useful, but I think the generated HTML file is a
step forward from the cut-and-paste solution currently in use.
2015-01-01 17:47:47 -05:00
Bradley M. Kuhn
7198b854b9 Add SellerEmail variable for testing data.
There are some situations were having the email address of the seller is
useful for testing.  This change allows for it as a parameter to the
tests.
2015-01-01 17:47:38 -05:00
Bradley M. Kuhn
5f4261beda ProfileID is a valid search term for TransactionSearch in the PayPal SOAP API:
I learned this by reading:
 https://developer.paypal.com/webapps/developer/docs/classic/api/merchant/TransactionSearch_API_Operation_SOAP/
2015-01-01 17:47:22 -05:00
8 changed files with 359 additions and 5 deletions

1
.gitignore vendored
View file

@ -4,4 +4,5 @@ auth.txt
blib blib
perltidy.LOG perltidy.LOG
pm_to_blib pm_to_blib
subscription-payment.html
options-payment.html options-payment.html

View file

@ -9,6 +9,8 @@ Revision history for Perl module Business::PayPal::API
- Extract out more payer details from XML. - Extract out more payer details from XML.
(PayerName, NameSuffix, PayerCountry). (PayerName, NameSuffix, PayerCountry).
- Fix https://rt.cpan.org/Public/Bug/Display.html?id=67386 - Fix https://rt.cpan.org/Public/Bug/Display.html?id=67386
- Options fields of GetTransactionDetails are now returned as a hash,
containing the actual options data, rather than array of empty strings.
0.70 2012-11-13 0.70 2012-11-13

12
Makefile.PL Normal file
View file

@ -0,0 +1,12 @@
use 5.008001;
use ExtUtils::MakeMaker;
# See lib/ExtUtils/MakeMaker.pm for details of how to influence
# the contents of the Makefile that is written.
WriteMakefile(
NAME => 'Business::PayPal::API',
VERSION_FROM => 'lib/Business/PayPal/API.pm',
PREREQ_PM => { SOAP::Lite => 0.67 },
($] >= 5.005 ?
(ABSTRACT_FROM => 'lib/Business/PayPal/API.pm',
AUTHOR => 'Scott Wiersdorf <scott@perlcode.org>') : ()),
);

View file

@ -0,0 +1,178 @@
# This file is part of Business:PayPal:API Module. License: Same as Perl. See its README for details.
package Business::PayPal::API::GetRecurringPaymentsProfileDetails;
use 5.008001;
use strict;
use warnings;
use SOAP::Lite 0.67;
use Business::PayPal::API ();
our @ISA = qw(Business::PayPal::API);
our @EXPORT_OK = qw(GetRecurringPaymentsProfileDetails); ## fake exporter
sub GetRecurringPaymentsProfileDetails {
my $self = shift;
my %args = @_;
my @trans =
(
$self->version_req,
SOAP::Data->name( ProfileID => $args{ProfileID} )->type( 'xs:string' ),
);
my $request = SOAP::Data->name
( GetRecurringPaymentsProfileDetailsRequest => \SOAP::Data->value( @trans ) )
->type("ns:GetRecurringPaymentsProfileDetailsRequestType");
my $som = $self->doCall( GetRecurringPaymentsProfileDetailsReq => $request )
or return;
my $path = '/Envelope/Body/GetRecurringPaymentsProfileDetailsResponse';
my %response = ();
unless( $self->getBasic($som, $path, \%response) ) {
$self->getErrors($som, $path, \%response);
return %response;
}
$path .= '/GetRecurringPaymentsProfileDetailsResponseDetails';
$self->getFields($som, $path, \%response,
{ ProfileID => 'ProfileID',
ProfileStatus => 'ProfileStatus',
Description => 'Description',
AggregateAmount => 'AggregateAmount',
RegularAmountPaid => 'RegularAmountPaid',
FinalPaymentDueDate => 'FinalPaymentDueDate',
TrialAmountPaid => 'TrialAmountPaid',
AggregateOptionalAmount => 'AggregateOptionalAmount',
AutoBillOutstandingAmount => 'AutoBillOutstandingAmount',
MaxFailedPayments => 'MaxFailedPayments',
CountryName => 'SubscriberShippingAddress/CountryName',
CityName => 'SubscriberShippingAddress/CityName',
Street1 => 'SubscriberShippingAddress/Street1',
Street2 => 'SubscriberShippingAddress/Street2',
PostalCode => 'SubscriberShippingAddress/PostalCode',
AddressID => 'SubscriberShippingAddress/AddressID',
ExternalAddressID => 'SubscriberShippingAddress/ExternalAddressID',
AddressOwner => 'SubscriberShippingAddress/AddressOwner',
Phone => 'SubscriberShippingAddress/Phone',
AddressStatus => 'SubscriberShippingAddress/AddressStatus',
Name => 'SubscriberShippingAddress/Name',
StateOrProvince => 'SubscriberShippingAddress/StateOrProvince',
BillingStartDate => 'BillingStartDate',
SubscriberName => 'SubscriberName',
TaxAmount => 'CurrentRecurringPaymentsPeriod/TaxAmount',
Amount => 'CurrentRecurringPaymentsPeriod/Amount',
ShippingAmount => 'CurrentRecurringPaymentsPeriod/ShippingAmount',
TotalBillingCycles => 'CurrentRecurringPaymentsPeriod/TotalBillingCycles',
BillingPeriod => 'CurrentRecurringPaymentsPeriod/BillingPeriod',
# 'LastPaymentAmount' => '10.00',
# 'NextBillingDate' => '2014-01-17T10:00:00Z',
# 'NumberCyclesRemaining' => '-2',
# 'NumberCyclesCompleted' => '2',
# 'LastPaymentDate' => '2013-12-17T10:59:31Z',
# 'FailedPaymentCount' => '0',
# 'OutstandingBalance' => '0.00'
# }, 'RecurringPaymentsSummaryType' ),
# '{urn:ebay:apis:eBLBaseComponents}RecurringPaymentsSummary',
# bless( {
# 'TaxAmount' => '0.00',
# 'Amount' => '10.00',
# 'ShippingAmount' => '0.00',
# 'TotalBillingCycles' => '0',
# 'BillingFrequency' => '1',
# 'BillingPeriod' => 'Month'
# }, 'BillingPeriodDetailsType' ),
# '{urn:ebay:apis:eBLBaseComponents}RegularRecurringPaymentsPeriod',
}
);
return %response;
}
1;
__END__
=head1 NAME
Business::PayPal::API::GetRecurringPaymentsProfileDetails - PayPal GetRecurringPaymentsProfileDetails API
=head1 SYNOPSIS
use Business::PayPal::API::GetRecurringPaymentsProfileDetails;
my $pp = new Business::PayPal::API::GetRecurringPaymentsProfileDetails ( ... );
or
## see Business::PayPal::API documentation for parameters
use Business::PayPal::API qw(GetRecurringPaymentsProfileDetails);
my $pp = new Business::PayPal::API( ... );
my %response = $pp->GetRecurringPaymentsProfileDetails( ProfileID => $profileID, );
=head1 DESCRIPTION
B<Business::PayPal::API::GetRecurringPaymentsProfileDetails> implements PayPal's
B<GetRecurringPaymentsProfileDetails> API using SOAP::Lite to make direct API calls to
PayPal's SOAP API server. It also implements support for testing via
PayPal's I<sandbox>. Please see L<Business::PayPal::API> for details
on using the PayPal sandbox.
=head2 GetRecurringPaymentsProfileDetails
Implements PayPal's B<GetRecurringPaymentsProfileDetails> API call. Supported
parameters include:
TransactionID
as described in the PayPal "Web Services API Reference" document.
Returns a hash containing the transaction details, including these fields:
As described in the API document.
Example:
my %resp = $pp->GetRecurringPaymentsProfileDetails( ProfileID => $trans_id );
print "Payer: $resp{ProfileStatus}\n";
=head2 ERROR HANDLING
See the B<ERROR HANDLING> section of B<Business::PayPal::API> for
information on handling errors.
=head2 EXPORT
None by default.
=head1 SEE ALSO
L<https://developer.paypal.com/en_US/pdf/PP_APIReference.pdf>
=head1 AUTHOR
Bradley M. Kuhn E<lt>bkuhn@ebb.orgE<gt>
adapted from work by:
Scot Wiersdorf E<lt>scott@perlcode.orgE<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2006 by Scott Wiersdorf
Copyright (C) 2013 by Bradley M. Kuhn
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.5 or,
at your option, any later version of Perl 5 you may have available.
=cut

View file

@ -127,12 +127,43 @@ sub GetTransactionDetails {
Number => 'Number', Number => 'Number',
Quantity => 'Quantity', Quantity => 'Quantity',
Amount => 'Amount', Amount => 'Amount',
Options => 'Options',
} }
); );
if ( scalar( @$paymentitems ) > 0 ) { if ( scalar( @$paymentitems ) > 0 ) {
# Options data must be extracted differently. Specifically, the
# "interesting" parts of the Options data is not in the values inside
# the $som structure (which means $som->valueof(), which
# $self->getFieldsList() above calls underneath, won't extract the
# useful data, because the values are empty.
# The convoluted loop below finds any Options values and matches them
# up properly with the correct PaymentItem. Note that the loop is
# written this way to account for the fact that there may be multiple
# PaymentItems.
# Finally, I contemplated placing this loop below in getFieldsList()
# with a special-case for Options, but I am not sure it belongs
# there. Ultimately, I think this is unique to the
# GetTransactionsDetails call in the API, and thus it's more
# appropriately placed here.
my $ii = 0;
my @fulloptions;
foreach my $rec ( $som->dataof($path . '/PaymentItemInfo/PaymentItem' ) ) {
my %options;
foreach my $subrec ($rec->value()) {
foreach my $fieldrec ($$subrec->value()) {
$options{$fieldrec->attr()->{name}} = $fieldrec->attr()->{value}
if ($fieldrec->name() eq "Options");
}
}
$paymentitems->[$ii]{Options} = \%options;
push(@fulloptions, \%options);
}
# Now, we can save the payment items properly
$response{PaymentItems} = $paymentitems; $response{PaymentItems} = $paymentitems;
# And set the PII_Options properly too.
$response{PII_Options} = \@fulloptions;
} }
return %response; return %response;
@ -260,6 +291,7 @@ records:
Number => '...', Number => '...',
Quantity => '...', Quantity => '...',
Amount => '...', Amount => '...',
Options => { 'key' => 'value', ... },
}, },
{ SalesTax => ..., etc. { SalesTax => ..., etc.
} ] } ]
@ -272,6 +304,9 @@ Example:
for my $item ( @{ $resp{PaymentItems} } ) { for my $item ( @{ $resp{PaymentItems} } ) {
print "Name: " . $item->{Name} . "\n"; print "Name: " . $item->{Name} . "\n";
print "Amt: " . $item->{Amount} . "\n"; print "Amt: " . $item->{Amount} . "\n";
for my $optionname (keys %$item->{Options}) {
print "Option: $optionname is " . $item->{Options}{$optionname} . "\n";
}
} }
=head2 ERROR HANDLING =head2 ERROR HANDLING
@ -290,10 +325,12 @@ L<https://developer.paypal.com/en_US/pdf/PP_APIReference.pdf>
=head1 AUTHOR =head1 AUTHOR
Scot Wiersdorf E<lt>scott@perlcode.orgE<gt> Scot Wiersdorf E<lt>scott@perlcode.orgE<gt>
Bradley M. Kuhn E<lt>bkuhn@ebb.orgE<gt>
=head1 COPYRIGHT AND LICENSE =head1 COPYRIGHT AND LICENSE
Copyright (C) 2006 by Scott Wiersdorf Copyright (C) 2006 by Scott Wiersdorf
Copyright (C) 2014, 2015 by Bradley M. Kuhn
This library is free software; you can redistribute it and/or modify This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.5 or, it under the same terms as Perl itself, either Perl version 5.8.5 or,

View file

@ -20,6 +20,7 @@ sub TransactionSearch {
Payer => 'ebl:EmailAddressType', Payer => 'ebl:EmailAddressType',
Receiver => 'ebl:EmailAddressType', Receiver => 'ebl:EmailAddressType',
ReceiptID => 'xs:string', ReceiptID => 'xs:string',
ProfileID => 'xs:string',
TransactionID => 'xs:string', TransactionID => 'xs:string',
InvoiceID => 'xs:string', InvoiceID => 'xs:string',
PayerName => 'xs:string', PayerName => 'xs:string',

View file

@ -10,7 +10,7 @@ if ( !$ENV{WPP_TEST} || !-f $ENV{WPP_TEST} ) {
'No WPP_TEST env var set. Please see README to run tests'; 'No WPP_TEST env var set. Please see README to run tests';
} }
else { else {
plan tests => 6; plan tests => 14;
} }
use_ok( 'Business::PayPal::API::TransactionSearch' ); use_ok( 'Business::PayPal::API::TransactionSearch' );
@ -75,10 +75,14 @@ foreach my $record (@{$resp}) {
like($detail{PaymentItems}[0]{Name}, qr/Field\s+Options/i, 'Found field options test transaction'); like($detail{PaymentItems}[0]{Name}, qr/Field\s+Options/i, 'Found field options test transaction');
like($detail{PII_Name}, qr/Field\s+Options/i, 'Found field options test transaction'); like($detail{PII_Name}, qr/Field\s+Options/i, 'Found field options test transaction');
foreach my $options ($detail{PaymentItems}[0]{Options}, $detail{PII_Options}) { foreach my $options ($detail{PaymentItems}[0]{Options}, $detail{PII_Options}[0]) {
ok((scalar(@$options) == 2 and $options->[0] eq '' and $options->[0] eq ''), ok(scalar(keys %$options) == 2, "The PaymentItems Options has 2 elements");
"PaymentItems's Options has 2 elements with empty strings"); ok(defined $options->{firstOption}, "'firstOption' is present");
ok($options->{firstOption} eq 'Yes', "'firstOption' is selected as 'Yes'");
ok(defined $options->{size}, "'size' option is present");
ok($options->{size} eq "Large", "'size' option is selected as 'Large'");
} }
# Local Variables: # Local Variables:
# Mode: CPerl # Mode: CPerl
# indent-tabs-mode: nil # indent-tabs-mode: nil

119
t/SubscriptionPayments.t Normal file
View file

@ -0,0 +1,119 @@
# -*- mode: cperl -*-
use strict;
use warnings;
use autodie qw(:all);
use Cwd;
use List::AllUtils;
use Test::More;
if ( !$ENV{WPP_TEST} || !-f $ENV{WPP_TEST} ) {
plan skip_all =>
'No WPP_TEST env var set. Please see README to run tests';
}
else {
plan tests => 5;
}
use_ok( 'Business::PayPal::API::TransactionSearch' );
#########################
require 't/API.pl';
my %args = do_args();
=pod
The following four tests shows the methodology to use TransactionSearch to
find transactions that are part of the same ProfileID. This method was
discovered by trial-and-error. Specifically, it's somewhat odd that
TransactionSearch is used with the parameter of 'ProfileID' with the value
set to a specific TransactionID to find the ProfileID via the "Created"
transaction. Then, in turn, that ProfileID can find the subscription payments
related to the original transaction.
This works, and seems to be correct, albeit odd.
=cut
open SUBSCRIPTION_PAY_HTML, '>', 'subscription-payment.html';
print SUBSCRIPTION_PAY_HTML <<_SUBSCRIPTION_PAYMENT_DATA_
<html>
<body>
<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="business" value="$args{SellerEmail}" />
<input type="hidden" name="item_name" value="Monthly Payment" />
<input type="hidden" name="cmd" value="_xclick-subscriptions">
<input id="no_shipping" type="hidden" name="no_shipping" value="0" />
<input type="hidden" name="lc" value="US">
<input type="hidden" name="no_note" value="1">
<input type="hidden" name="t3" value="M" />
<input type="hidden" name="p3" value="1" />
<input type="hidden" name="src" value="1" />
<input type="hidden" name="srt" value="0" />
<input id="no_shipping" type="hidden" name="no_shipping" value="0" />
<input type="hidden" name="no_note" value="1">
<input id="amount" type="text" name="a3" size="5" minimum="10" value="10" />
<input type="image" border="0" name="submit" alt="Make test monthly payment now">
</form>
</body>
</html>
_SUBSCRIPTION_PAYMENT_DATA_
;
close SUBSCRIPTION_PAY_HTML;
my $cwd = getcwd;
print STDERR <<"_PROFILEID_";
Please note the next series of tests will not succeeed unless there is at
least one transaction that is part of a subscription payments in your business
account.
if you haven't made one yet, you can visit:
file:///$cwd/subscription-payment.html
and use the sandbox buyer account to make the payment.
_PROFILEID_
my $start_date = '1998-01-01T01:45:10.00Z';
my $ts = Business::PayPal::API::TransactionSearch->new( %args );
my $resp = $ts->TransactionSearch( StartDate => $start_date );
ok( scalar @{$resp} > 0, 'Some transactions found' );
my ( $profileID, %possible_txn_ids );
foreach my $record ( @{$resp} ) {
if ( $record->{Type} =~ /Recurring/ ) {
if ( $record->{Status} =~ /Completed/ ) {
$possible_txn_ids{ $record->{TransactionID} } = $record;
}
elsif ( $record->{Status} =~ /Created/ ) {
$profileID = $record->{TransactionID};
}
}
}
ok( defined $profileID,
'Subscription Payment Creation Record and ProfileID Found' );
ok( scalar keys %possible_txn_ids > 0,
'Subscription Payment Transactions Found'
);
my $date_search_res = $ts->TransactionSearch(
ProfileID => $profileID,
StartDate => $start_date,
);
# One of these will need to be in the possibleTransactionID list (i.e., we're
# assuming that at least one payment has occured in this repeating).
ok( List::AllUtils::any {
defined $possible_txn_ids{ $_->{TransactionID} }
}
@{$date_search_res},
'Found one payment transaction under the given Profile ID'
);