File Coverage

blib/lib/Marketplace/Ebay.pm
Criterion Covered Total %
statement 18 20 90.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 25 27 92.5


line stmt bran cond sub pod time code
1             package Marketplace::Ebay;
2              
3 1     1   21350 use 5.010001;
  1         4  
4 1     1   6 use strict;
  1         1  
  1         25  
5 1     1   4 use warnings FATAL => 'all';
  1         10  
  1         38  
6              
7 1     1   801 use HTTP::Thin;
  1         142703  
  1         38  
8 1     1   923 use HTTP::Request;
  1         1278  
  1         34  
9 1     1   7 use HTTP::Headers;
  1         2  
  1         34  
10 1     1   6925 use XML::LibXML;
  0            
  0            
11             use XML::Compile::Schema;
12             use XML::Compile::Util qw/pack_type/;
13             use Data::Dumper;
14             use XML::LibXML::Simple;
15             use Marketplace::Ebay::Response;
16             use Marketplace::Ebay::Order;
17              
18             use Moo;
19             use MooX::Types::MooseLike::Base qw(:all);
20             use namespace::clean;
21              
22             =head1 NAME
23              
24             Marketplace::Ebay - Making API calls to eBay (with XSD validation)
25              
26             =head1 VERSION
27              
28             Version 0.18
29              
30             =cut
31              
32             our $VERSION = '0.18';
33              
34             =head1 SYNOPSIS
35              
36             my $ebay = Marketplace::Ebay->new(
37             production => 0,
38             site_id => 77,
39             developer_key => '1234',
40             application_key => '6789',
41             certificate_key => '6666',
42             token => 'asd12348',
43             xsd_file => 'ebay.xsd',
44             );
45             my $res = $ebay->api_call('GeteBayOfficialTime', {});
46             print Dumper($res);
47              
48              
49             L
50              
51             =head1 DESCRIPTION
52              
53             =head2 International Selling
54              
55             Products propagate from your local selling account to all Ebay sites
56             where you are going to ship your items to:
57              
58             L
59              
60             =head1 ACCESSORS
61              
62             =head2 Credentials
63              
64             The following are required for a successful API call.
65              
66             =head3 developer_key
67              
68             =head3 application_key
69              
70             =head3 certificate_key
71              
72             =head3 token
73              
74             =head3 site_id
75              
76             The id of the site. E.g., Germany is 77.
77              
78             L
79              
80             =head3 xsd_file
81              
82             Path to the XSD file with the eBay definitions.
83              
84             http://developer.ebay.com/webservices/latest/ebaySvc.xsd
85              
86             =head3 production
87              
88             Boolean. Default to false.
89              
90             By default, the API calls are done against the sandbox. Set it to true
91             in production.
92              
93             =head3 endpoint
94              
95             Set lazily by the class depending on the C value.
96              
97             =head3 compatibility_level
98              
99             The version of API and XSD used. Please keep this in sync with the XSD.
100              
101             =head3 last_response
102              
103             You can get the HTTP::Response object of the last call using this
104             accessor.
105              
106             =head2 last_request
107              
108             You can get the HTTP::Request object of the last call using this
109             accessor.
110              
111             =head3 last_parsed_response
112              
113             Return a L object (or undef on failure)
114             out of the return value of the last C.
115              
116             =head3 log_file
117              
118             A filename where to log requests and responses.
119              
120             =cut
121              
122             has developer_key => (is => 'ro', required => 1);
123             has application_key => (is => 'ro', required => 1);
124             has certificate_key => (is => 'ro', required => 1);
125             has token => (is => 'ro', required => 1);
126             has site_id => (is => 'ro', required => 1);
127             has site_code => (is => 'lazy');
128              
129             sub _build_site_code {
130             my $self = shift;
131             my $code = $self->ebay_sites_id_to_name->{$self->site_id};
132             die $self->site_id . " doesn't map to a site name!" unless $code;
133             return $code;
134             }
135              
136             # totally random, as per Net::eBay
137             has compatibility_level => (is => 'ro',
138             default => sub { '655' });
139              
140             has session_certificate => (is => 'lazy');
141             has production => (is => 'ro', default => sub { 0 });
142             has endpoint => (is => 'lazy');
143              
144             has last_response => (is => 'rwp');
145             has last_parsed_response => (is => 'rwp');
146             has last_request => (is => 'rwp');
147             has log_file => (is => 'rw');
148              
149             sub _build_endpoint {
150             my $self = shift;
151             if ($self->production) {
152             return 'https://api.ebay.com/ws/api.dll';
153             }
154             else {
155             return 'https://api.sandbox.ebay.com/ws/api.dll';
156             }
157             }
158              
159             sub _build_session_certificate {
160             my $self = shift;
161             return join(';',
162             $self->developer_key,
163             $self->application_key,
164             $self->certificate_key);
165             }
166              
167             has xsd_file => (is => 'ro', required => 1);
168              
169             has schema => (is => 'lazy');
170              
171             sub _build_schema {
172             my $self = shift;
173             my $xsd_file = $self->xsd_file;
174             die "$xsd_file is not a file" unless -f $xsd_file;
175             return XML::Compile::Schema->new($self->xsd_file);
176             }
177              
178              
179             =head1 METHODS
180              
181             =head2 api_call($name, \%data, \%options)
182              
183             Do the API call $name with payload in %data. Return the data structure
184             of the L object. In case of failure,
185             return nothing. In this case, you can inspect the details of the
186             failure inspecting, e.g.,
187              
188             $self->last_response->status_line;
189              
190             With option C set to a true value, the method doesn't
191             return the object, but a plain hashref with the structure returned
192             (old behaviour).
193              
194             With an option C, the XML is parsed using XMLin, not the
195             schema, so the result could be unstable, but on the other hand, you
196             get something out of XML which doesn't comply with the schema.
197              
198             =head2 prepare_xml($name, \%data)
199              
200             Create the XML document to send for the API call $name.
201              
202             =head2 show_xml_template($call, $call_type)
203              
204             Utility for development. Show the expected structure for the API call
205             $call. The second argument is optional, and may be Request or
206             Response, defaulting to Request.
207              
208             =head2 log_event(@data)
209              
210             Append the arguments to the C, if it was defined.
211              
212             =cut
213              
214             sub api_call {
215             my ($self, $call, $data, $options) = @_;
216             $options ||= {};
217             $self->log_event("Preparing call to $call for " . Dumper($data));
218             my $xml = $self->prepare_xml($call, $data);
219             my $headers = $self->_prepare_headers($call);
220             my $request = HTTP::Request->new(POST => $self->endpoint, $headers, $xml);
221             $self->_set_last_request($request);
222             $self->log_event("Doing $call request\n" . $request->as_string);
223             my $response = HTTP::Thin->new->request($request);
224             $self->_set_last_response($response);
225             $self->log_event("Retrieving $call response\n" . $response->as_string);
226             $self->_set_last_parsed_response(undef);
227             if ($response->is_success) {
228             my $struct;
229             if ($options->{no_validate}) {
230             $struct = XMLin($response->decoded_content);
231             }
232             else {
233             $struct = $self->_parse_response($call, $response->decoded_content)
234             }
235             if ($struct) {
236             my $obj = Marketplace::Ebay::Response->new(struct => $struct);
237             $self->_set_last_parsed_response($obj);
238             $self->log_event("Got response:" . Dumper($struct));
239             if ($options->{requires_struct}) {
240             return $struct;
241             }
242             else {
243             return $obj;
244             }
245             }
246             }
247             return;
248             }
249              
250             sub _parse_response {
251             my ($self, $call, $xml) = @_;
252             my $reader = $self->schema->compile(READER => $self->_xml_type($call,
253             'Response'));
254             my $struct;
255             eval {
256             $struct = $reader->($xml);
257             };
258             return $struct;
259             }
260              
261             sub log_event {
262             my ($self, @strings) = @_;
263             if (my $file = $self->log_file) {
264             open (my $fh, '>>', $file) or die "Cannot open $file $!";
265             my $now = "\n" . localtime() . "\n";
266             print $fh $now, @strings;
267             close $fh or die "Cannot close $file $!";
268             }
269             }
270              
271             sub _prepare_headers {
272             my ($self, $call) = @_;
273             my $headers = HTTP::Headers->new;
274             $headers->push_header('X-EBAY-API-COMPATIBILITY-LEVEL' => $self->compatibility_level);
275             $headers->push_header('X-EBAY-API-DEV-NAME' => $self->developer_key);
276             $headers->push_header('X-EBAY-API-APP-NAME' => $self->application_key);
277             $headers->push_header('X-EBAY-API-CERT-NAME' => $self->certificate_key);
278             $headers->push_header('X-EBAY-API-CALL-NAME' => $call);
279             $headers->push_header('X-EBAY-API-SITEID' => $self->site_id);
280             $headers->push_header('Content-Type' => 'text/xml');
281             return $headers;
282             }
283              
284             sub show_xml_template {
285             my ($self, $call, $call_type) = @_;
286             return $self->schema->template(PERL => $self->_xml_type($call, $call_type),
287             use_default_namespace => 1);
288             }
289              
290             sub _xml_type {
291             my ($self, $call, $call_type) = @_;
292             $call_type ||= 'Request';
293             die "Second argument must be Request or Response, defaulting to Request"
294             unless ($call_type eq 'Request' or $call_type eq 'Response');
295             return pack_type('urn:ebay:apis:eBLBaseComponents', $call . $call_type);
296             }
297              
298             sub prepare_xml {
299             my ($self, $name, $data) = @_;
300             $data ||= {};
301             # inject the token
302             $data->{RequesterCredentials}->{eBayAuthToken} = $self->token;
303             my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
304             my $type = $self->_xml_type($name);
305             my $write = $self->schema->compile(WRITER => $type,
306             use_default_namespace => 1);
307             my $xml = $write->($doc, $data);
308             $doc->setDocumentElement($xml);
309             return $doc->toString(1);
310             }
311              
312             =head1 CONVENIENCE METHODS
313              
314             =cut
315              
316             =head2 api_call_wrapper($call, $data, @ids)
317              
318             Call Ebay's API $call with data $data. Then check the response and
319             return it. This ends calling $self->api_call($call, $data), but after
320             that the response is checked and diagnostics are printed out. If you
321             want something quiet and which is not going to die, or you're
322             expecting invalid structures, you should use C directly.
323              
324             Return the response object. If no response object is found, it will
325             die.
326              
327             =cut
328              
329             sub api_call_wrapper {
330             my ($self, $call, $data, @identifiers) = @_;
331             my $res = $self->api_call($call, $data);
332             my $message = $call;
333             if (@identifiers) {
334             $message .= " on " . join(' ', @identifiers);
335             }
336             if ($res) {
337             if ($res->is_success) {
338             print "$message OK\n";
339             }
340             elsif ($res->errors) {
341             warn "$message:\n" . $res->errors_as_string;
342             }
343             else {
344             die "$message: Nor success, nor errors!" . Dumper($res);
345             }
346             if (my $item_id = $res->item_id) {
347             print "$message: ebay id: $item_id\n";
348             }
349             my $fee = $res->total_listing_fee;
350             if (defined $fee) {
351             print "$message Fee is $fee\n";
352             }
353             }
354             else {
355             die "No response found!" . $self->last_response->status_line
356             . "\n" . $self->last_response->content;
357             }
358             return $res;
359             }
360              
361             =head2 cancel_item($identifier, $id, $reason)
362              
363             $identifier is mandatory and can be C or C (depending if
364             you do tracking by sku (this is possible only if the sku was uploaded
365             with InventoryTrackingMethod = SKU) or by ebay item id. The $id is
366             mandatory and is the sku or the ebay_id.
367              
368             Reason can be one of the following, defaulting to OtherListingError: Incorrect, LostOrBroken, NotAvailable, OtherListingError, SellToHighBidder, Sold.
369              
370             It calls EndFixedPriceItem, so this method is useful only for shops.
371              
372             =cut
373              
374             sub cancel_item {
375             my ($self, $key, $value, $reason) = @_;
376             die "Missing SKU or ItemID" unless $key;
377             my %mapping = (
378             SKU => 'sku',
379             ItemID => 'ebay_sku',
380             );
381             my %reasons = (
382             Incorrect => 1,
383             LostOrBroken => 1,
384             NotAvailable => 1,
385             OtherListingError => 1,
386             SellToHighBidder => 1,
387             Sold => 1,
388             );
389             die "Invalid key $key" unless $mapping{$key};
390             unless ($reason && $reasons{$reason}) {
391             $reason = 'OtherListingError';
392             }
393             die "Missing $key" unless $value;
394             my $res = $self
395             ->api_call_wrapper(EndFixedPriceItem => {
396             EndingReason => $reason,
397             ErrorLanguage => 'en_US',
398             $key => $value,
399             }, $key, $value);
400             return $res;
401             }
402              
403             =head2 delete_sku_variations($sku, \@list_of_sku_variations)
404              
405             It uses ReviseFixedPriceItem to cancel variations of a given sku, not
406             passing asking for a deletion, but instead setting the quantity to 0.
407             This because deleting a variation is not possible if a purchase has
408             been done against it.
409              
410             =cut
411              
412             sub delete_sku_variations {
413             my ($self, $sku, $list) = @_;
414             die unless $sku && $list;
415             my @delete = map { +{
416             Quantity => 0,
417             SKU => $_,
418             } } @$list;
419             my $data = {
420             ErrorLanguage => 'en_US',
421             WarningLevel => 'High',
422             Item => {
423             SKU => $sku,
424             Variations => {
425             Variation => \@delete,
426             }
427             },
428             };
429             my $res = $self->api_call_wrapper(ReviseFixedPriceItem => $data, $sku, "delete variations");
430             return $res;
431             }
432              
433             =head2 get_category_specifics($id)
434              
435             Return a dump of the structure found in the GetCategorySpecifics
436              
437             =cut
438              
439             sub get_category_specifics {
440             my ($self, $id) = @_;
441             die "Missing category id" unless $id;
442             my $res = $self->api_call_wrapper(GetCategorySpecifics => {
443             CategoryID => $id,
444             }, "category id $id");
445             return $res;
446             }
447              
448             =head2 get_orders($number_of_days)
449              
450             Retrieve the last orders in the last number of days, defaulting to 7.
451             Return a list of L objects. You can access
452             the raw structures with $object->order.
453              
454             =cut
455              
456             sub get_orders {
457             my ($self, $backlog) = @_;
458             $backlog ||= 7;
459             my $request = {
460             NumberOfDays => $backlog,
461             Pagination => {
462             PageNumber => 1,
463             EntriesPerPage => 100,
464             },
465             ErrorLanguage => 'en_US',
466             };
467             my $repeat = 1;
468             my @orders;
469             do {
470             my $obj = $self->api_call_wrapper(GetOrders => $request);
471             my $res = $obj->struct;
472             if (exists $res->{OrderArray} and
473             exists $res->{OrderArray}->{Order}) {
474             foreach my $ord (@{$res->{OrderArray}->{Order}}) {
475             push @orders, Marketplace::Ebay::Order->new(order => $ord);
476             }
477             }
478             $repeat = $res->{HasMoreOrders};
479             $request->{Pagination}->{PageNumber}++;
480             } while ($repeat);
481              
482             return @orders;
483             }
484              
485             =head2 get_orders_for_site
486              
487             Like get_orders, but filter the orders by the site_id of the object
488             (otherwise you pull the orders from all the international sites).
489              
490             =cut
491              
492             sub get_orders_for_site {
493             my ($self, $backlog) = @_;
494             my @orders = $self->get_orders($backlog);
495             my $name = $self->site_code;
496             return grep { $_->ebay_site eq $name } @orders;
497             }
498              
499             =head2 ebay_site_code_types (internal)
500              
501             L
502             turned into an hashref to map the site name to an id.
503              
504             =head2 ebay_sites_name_to_id
505              
506             Return an hashref for mapping ebay site names and abbreviations to a numeric id.
507              
508             =head2 ebay_sites_id_to_name
509              
510             Return an hashref for mapping ebay site id to its name.
511             # http://developer.ebay.com/devzone/XML/docs/Reference/ebay/types/SiteCodeType.html
512              
513             =cut
514              
515             sub ebay_site_code_types {
516              
517             my %codes = (
518             Australia => { id => 15, abbreviation => "AU" },
519             Austria => { id => 16, abbreviation => "AT" },
520             Belgium_Dutch => { id => 123, abbreviation => "BENL" },
521             Belgium_French => { id => 23, abbreviation => "BEFR" },
522             Canada => { id => 2, abbreviation => "CA" },
523             CanadaFrench => { id => 210, abbreviation => "CAFR" },
524             France => { id => 71, abbreviation => "FR" },
525             Germany => { id => 77, abbreviation => "DE" },
526             HongKong => { id => 201, abbreviation => "HK" },
527             India => { id => 203, abbreviation => "IN" },
528             Ireland => { id => 205, abbreviation => "IE" },
529             Italy => { id => 101, abbreviation => "IT" },
530             Malaysia => { id => 207, abbreviation => "MY" },
531             Netherlands => { id => 146, abbreviation => "NL" },
532             Philippines => { id => 211, abbreviation => "PH" },
533             Poland => { id => 212, abbreviation => "PL" },
534             Russia => { id => 215, abbreviation => "RU" },
535             Singapore => { id => 216, abbreviation => "SG" },
536             Spain => { id => 186, abbreviation => "ES" },
537             Switzerland => { id => 193, abbreviation => "CH" },
538             UK => { id => 3, abbreviation => "UK" },
539             US => { id => 0, abbreviation => "US" },
540             );
541             return %codes;
542             }
543              
544             sub ebay_sites_name_to_id {
545             my $self = shift;
546             my %out;
547             my %codes = $self->ebay_site_code_types;
548             foreach my $k (keys %codes) {
549             $out{$k} = $codes{$k}{id};
550             $out{$codes{$k}{abbreviation}} = $codes{$k}{id};
551             }
552             return \%out;
553             }
554              
555             sub ebay_sites_id_to_name {
556             my $self = shift;
557             my %codes = $self->ebay_site_code_types;
558             my %out;
559             foreach my $k (keys %codes) {
560             $out{$codes{$k}{id}} = $k;
561             }
562             return \%out;
563             }
564              
565             =head1 AUTHOR
566              
567             Marco Pessotto, C<< >>
568              
569             =head1 BUGS
570              
571             Please report any bugs or feature requests to C, or through
572             the web interface at L. I will be notified, and then you'll
573             automatically be notified of progress on your bug as I make changes.
574              
575              
576              
577              
578             =head1 SUPPORT
579              
580             You can find documentation for this module with the perldoc command.
581              
582             perldoc Marketplace::Ebay
583              
584              
585             You can also look for information at:
586              
587             =over 4
588              
589             =item * RT: CPAN's request tracker (report bugs here)
590              
591             L
592              
593             =item * AnnoCPAN: Annotated CPAN documentation
594              
595             L
596              
597             =item * CPAN Ratings
598              
599             L
600              
601             =item * Search CPAN
602              
603             L
604              
605             =back
606              
607              
608             =head1 ACKNOWLEDGEMENTS
609              
610              
611             =head1 LICENSE AND COPYRIGHT
612              
613             Copyright 2014 Marco Pessotto.
614              
615             This program is free software; you can redistribute it and/or modify it
616             under the terms of either: the GNU General Public License as published
617             by the Free Software Foundation; or the Artistic License.
618              
619             See L for more information.
620              
621              
622             =cut
623              
624             1; # End of Marketplace::Ebay