File Coverage

blib/lib/Amazon/MWS/XML/Product.pm
Criterion Covered Total %
statement 15 129 11.6
branch 0 86 0.0
condition 0 12 0.0
subroutine 5 17 29.4
pod 8 9 88.8
total 28 253 11.0


line stmt bran cond sub pod time code
1             package Amazon::MWS::XML::Product;
2              
3 1     1   1362 use strict;
  1         2  
  1         27  
4 1     1   4 use warnings;
  1         1  
  1         29  
5              
6 1     1   550 use URI;
  1         7643  
  1         30  
7 1     1   706 use Moo;
  1         12836  
  1         17  
8 1     1   1914 use MooX::Types::MooseLike::Base qw(:all);
  1         5939  
  1         2526  
9              
10             =head1 NAME
11              
12             Amazon::MWS::XML::Product
13              
14             =head1 DESCRIPTION
15              
16             Class to handle the products and emit data structures suitable for XML
17             generation.
18              
19             =head1 ACCESSORS
20              
21             They has to be passed to the constructor
22              
23             =over 4
24              
25             =item sku
26              
27             Mandatory.
28              
29             =item feeds_needed([qw/product inventory price image variants/])
30              
31             If set to an arrayref, output only the selected feeds.
32              
33             =item timestamp_string
34              
35             An arbitrary string (usually a timestamp) which identifies the
36             revision of the product.
37              
38             =item ean
39              
40             =item asin
41              
42             =item title
43              
44             =item description
45              
46             =item brand
47              
48             =item category_code
49              
50             =item product_data
51              
52             This accessor should contain category-specific structures. This
53             appears to be needed when creating a product which is not present on
54             Amazon.
55              
56             Example values:
57              
58             { CE => { ProductType => { PhoneAccessory => {} } } }
59              
60             { Sports => { ProductType => 'SportingGoods' } }
61              
62             The exect structure to pass can be determined only looking at the
63             specific xsd file.
64              
65             Please keep in mind that the category_code has nothing to do with this
66             structure, and doesn't even exist an exact mapping between these
67             categories and the listing categories.
68              
69             Documentation from Amazon:
70              
71             Section containing category-specific information such as variations.
72             Reference one or more of the following XSDs to complete the
73             ProductData section (only one category can be used for a given item).
74              
75             Keep in mind that some of these product categories might not be
76             available for merchants on some Amazon websites. If a product category
77             is available to merchants on a particular Amazon website, then the XSD
78             files for that category are valid for that Amazon website as well.
79              
80             =item inventory
81              
82             Indicates whether or not an item is available (any positive number =
83             available; 0 = not available). Every time a quantity is sent for an
84             item, the existing quantity is replaced by the new quantity in the
85             feed.
86              
87             This accessor is read-write because L<Amazon::MWS::Uploader> may want
88             to throttle the inventory. Other code is discouraged to use this as a
89             modifier.
90              
91             =item ship_in_days
92              
93             The number of days between the order date and the ship date (a whole
94             number between 1 and 30). If not specified the info will not be set
95             and Amazon will use a default of 2 business days, so we use the
96             default of 2 here.
97              
98             =item price
99              
100             The standard price of the item. If the price is zero, it is assumed to
101             be a product which should be set as inactive without removing it,
102             flipping the inventory to zero and refraining to do pass
103             images/variants/price feeds.
104              
105             The price is rounded via sprintf '%.2f' by the module.
106              
107             =item currency
108              
109             Valid values are: AUD BRL CAD CNY DEFAULT EUR GBP INR JPY MXN USD.
110              
111             Defaults to EUR.
112              
113             =item sale_price
114              
115             A sale price (optional)
116              
117             =item sale_start
118              
119             A DateTime object with the sale start date
120              
121             =item sale_end
122              
123             A DateTime object with the sale end date
124              
125             =item images
126              
127             An (optional) arrayref of image urls. The first will become the main
128             image, the other one will become the PT1, etc.
129              
130             Please note that B<only http:// links> are allowed. If you pass https://
131             links, they will be rejected by Amazon.
132              
133             =item children
134              
135             An (optional) arraryref of children sku.
136              
137             =item search_terms
138              
139             An (optional) arrayref of search terms (max 5)
140              
141             =item features
142              
143             An (optional) arrayref of strings with features (max 5)
144              
145             =item condition
146              
147             Possible values which validates correctly: Club CollectibleAcceptable
148             CollectibleGood CollectibleLikeNew CollectibleVeryGood New Refurbished
149             UsedAcceptable UsedGood UsedLikeNew UsedVeryGood
150              
151             Defaults to C<New>
152              
153             =item condition_note
154              
155             An arbitrary string shorter than 2000 characters with comments about
156             the condition.
157              
158             =item manufacturer
159              
160             Maker of the product (max 50 chars)
161              
162             =item manufacturer_part_number
163              
164             Part number manufacturer.
165              
166             =back
167              
168             =cut
169              
170             has sku => (is => 'ro', required => 1);
171              
172             has feeds_needed => (is => 'rw', isa => ArrayRef,
173             default => sub { [qw/product inventory price image variants/] });
174              
175             sub is_feed_needed {
176 0     0 0   my ($self, $feed) = @_;
177 0 0         return unless $feed;
178 0           return scalar(grep { $_ eq $feed } @{ $self->feeds_needed });
  0            
  0            
179             }
180              
181             has timestamp_string => (is => 'ro',
182             default => sub { '0' });
183             has ean => (is => 'ro',
184             isa => sub { _check_length($_[0], 8, 16) });
185              
186             has asin => (is => 'ro');
187              
188             has title => (is => 'ro',
189             isa => sub { _check_length($_[0], 1, 500) },
190             );
191             has description => (is => 'ro',
192             isa => sub { _check_length($_[0], 0, 2000) },
193             );
194             has brand => (is => 'ro',
195             isa => sub { _check_length($_[0], 0, 50) },
196             );
197             has condition => (is => 'ro',
198             default => sub { 'New' },
199             isa => sub {
200             my %condition_map = (
201             Club => 1,
202             CollectibleAcceptable => 1,
203             CollectibleGood => 1,
204             CollectibleLikeNew => 1,
205             CollectibleVeryGood => 1,
206             New => 1,
207             Refurbished => 1,
208             UsedAcceptable => 1,
209             UsedGood => 1,
210             UsedLikeNew => 1,
211             UsedVeryGood => 1,
212             );
213             my $cond = $_[0];
214             die "Unrecognized condition $cond, must be one of the following: "
215             . join(' ', keys %condition_map) unless $condition_map{$cond};
216             });
217             has condition_note => (
218             is => 'ro',
219             isa => sub { _check_length($_[0], 0, 2000) },
220             );
221             has category_code => (is => 'ro');
222             has product_data => (is => 'ro');
223             has manufacturer_part_number => (is => 'ro',
224             isa => sub { _check_length($_[0], 0, 40) }
225             );
226             has manufacturer => (is => 'ro',
227             isa => sub { _check_length($_[0], 0, 50) });
228              
229             has search_terms => (is => 'ro', isa => ArrayRef);
230             has features => (is => 'ro', isa => ArrayRef);
231              
232             sub _check_length {
233 0     0     my ($value, $min, $max) = @_;
234 0 0         if (defined $value) {
235 0 0         die "Max characters is $max" if length($value) > $max;
236 0 0         die "Min characters is $min" if length($value) < $min;
237             }
238             }
239              
240             sub _check_units {
241 0     0     my $unit = $_[0];
242 0           my %units = (
243             GR => 1,
244             KG => 1,
245             LB => 1,
246             MG => 1,
247             OZ => 1,
248             );
249             die "Wrong unit. Possible are :"
250 0 0         . join(" ", keys %units) unless $units{$unit};
251             }
252              
253             =over 4
254              
255             =item package_weight
256              
257             Weight of the package.
258              
259             =item package_weight_unit
260              
261             Unit for the package weight. Possible values are C<GR>, C<KG>, C<LB>,
262             C<MG>, C<OZ>. Defaults to C<GR>.
263              
264             =item shipping_weight
265              
266             Weight of the product when packaged to ship.
267              
268             =item shipping_weight_unit
269              
270             Unit for the package weight for shipping. Possible values are C<GR>,
271             C<KG>, C<LB>, C<MG>, C<OZ>. Defaults to C<GR>.
272              
273             =back
274              
275             =cut
276              
277             has package_weight => (is => 'ro');
278              
279             has package_weight_unit => (is => 'ro',
280             default => sub { 'GR' },
281             isa => \&_check_units,
282             );
283              
284             has shipping_weight => (is => 'ro');
285              
286             has shipping_weight_unit => (is => 'ro',
287             default => sub { 'GR' },
288             isa => \&_check_units,
289             );
290              
291             has inventory => (is => 'rw',
292             default => sub { '0' },
293             isa => Int);
294              
295             has ship_in_days => (is => 'ro',
296             isa => Int,
297             default => sub { '2' });
298              
299             has price => (is => 'ro',
300             required => 1,
301             isa => \&_validate_price);
302              
303             sub _validate_price {
304 0     0     my ($price) = @_;
305 0 0         die "$price is not a number" unless is_Num($price);
306 0 0         die "$price is negative" if $price < 0;
307             }
308              
309              
310             has sale_price => (is => 'ro',
311             isa => \&_validate_price);
312              
313             has sale_start => (is => 'ro',
314             isa => sub {
315             die "Not a datetime"
316             unless $_[0]->isa('DateTime');
317             });
318              
319             has sale_end => (is => 'ro',
320             isa => sub {
321             die "Not a datetime"
322             unless $_[0]->isa('DateTime');
323             });
324              
325             has currency => (is => 'ro',
326             isa => sub {
327             my %currency = map { $_ => 1 } (qw/AUD BRL CAD CNY DEFAULT
328             EUR GBP INR JPY MXN USD/);
329             die "Not a valid currency" unless $currency{$_[0]};
330             },
331             default => sub { 'EUR' });
332              
333             has images => (is => 'ro',
334             isa => sub {
335             die "Not an arrayref" unless is_ArrayRef($_[0]);
336             foreach my $url (@{ $_[0] }) {
337             my $check = URI->new($url)->as_string;
338             die "Non-URI character in url $url (should be $check)"
339             if $check ne $url;
340             }
341             });
342            
343             has children => (is => 'rw',
344             isa => ArrayRef);
345              
346              
347             # has restock_date => (is => 'ro');
348              
349              
350             =head1 METHODS
351              
352             =head2 price_is_zero
353              
354             Return true if the price is 0.
355              
356             =cut
357              
358             sub price_is_zero {
359 0     0 1   my $self = shift;
360 0           my $price = $self->price;
361 0 0         if ($price > 0) {
362 0           return 0;
363             }
364             else {
365 0           return 1;
366             }
367             }
368              
369             =head2 is_inactive
370              
371             Return true if price is 0 or inventory is 0. Inactive items will not
372             get a price, variants, image feed output.
373              
374             =cut
375              
376             sub is_inactive {
377 0     0 1   my $self = shift;
378 0 0 0       if ($self->price_is_zero or $self->inventory < 1) {
379 0           return 1;
380             }
381             else {
382 0           return;
383             }
384             }
385              
386              
387             =head2 as_product_hash
388              
389             Return a data structure suitable to feed the Product slot in a Product
390             feed.
391              
392             =head2 as_inventory_hash
393              
394             Return a data structure suitable to feed the Inventory slot in a
395             Inventory feed. Negative quantities will be normalized to 0.
396             Inactive products will get a quantity of 0.
397              
398             =head2 as_price_hash
399              
400             Return a data structure suitable to feed the Price slot in a Price
401             feed. If it's a inactive product, return nothing so there is a
402             chance that we don't need the price feed at all (if all products are
403             inactive).
404              
405             =cut
406              
407              
408             sub as_product_hash {
409 0     0 1   my $self = shift;
410 0 0         return unless $self->is_feed_needed('product');
411 0           my $data = {
412             SKU => $self->sku,
413             };
414 0 0         if (my $ean = $self->ean) {
415             $data->{StandardProductID} = {
416 0           Type => 'EAN',
417             Value => $ean,
418             }
419             }
420              
421 0           $data->{Condition} = { ConditionType => $self->condition };
422 0 0         if (my $cond_note = $self->condition_note) {
423 0           $data->{Condition}->{ConditionNote} = $cond_note;
424             }
425              
426             # how many items in a package
427             # $data->{ItemPackageQuantity} = 1
428             # and totally
429             # $data->{NumberOfItems} = 1
430              
431 0 0         if (my $title = $self->title) {
432 0           $data->{DescriptionData}->{Title} = $title;
433             }
434            
435 0 0         if (my $brand = $self->brand) {
436 0           $data->{DescriptionData}->{Brand} = $brand;
437             }
438 0 0         if (my $desc = $self->description) {
439 0           $data->{DescriptionData}->{Description} = $desc;
440             }
441 0 0         if (my $cat = $self->category_code) {
442 0           $data->{DescriptionData}->{RecommendedBrowseNode} = $cat;
443             }
444 0 0         if (my $manufacturer = $self->manufacturer) {
445 0           $data->{DescriptionData}->{Manufacturer} = $manufacturer;
446             }
447              
448 0 0         if (my $manufacturer_part = $self->manufacturer_part_number) {
449 0           $data->{DescriptionData}->{MfrPartNumber} = $manufacturer_part;
450             }
451 0 0         if (my $search_terms = $self->search_terms) {
452 0 0         if (my @terms = @$search_terms) {
453 0 0         if (@terms > 5) {
454 0           warn "Max terms is 5, removing some of them: " .
455             join(" ", splice(@terms, 5)) . "\n";
456             }
457 0           my @filtered = map { substr $_, 0, 50 } @terms;
  0            
458 0           $data->{DescriptionData}->{SearchTerms} = \@filtered;
459             }
460             }
461 0 0         if (my $features = $self->features) {
462 0 0         if (my @feats = grep { $_ } @$features) {
  0            
463 0 0         if (@feats > 5) {
464 0           warn "Max features is 5, removing some of them: \n";
465 0           warn join(" ", splice(@feats, 5)) . "\n";
466             }
467 0           $data->{DescriptionData}->{BulletPoint} = \@feats;
468             }
469             }
470              
471 0 0         if (my $weight = $self->package_weight) {
472 0           my $unit = $self->package_weight_unit;
473             $data->{DescriptionData}->{PackageWeight} = {
474 0           unitOfMeasure => $unit,
475             _ => $weight,
476             };
477             }
478 0 0         if (my $ship_weight = $self->shipping_weight) {
479 0           my $unit = $self->shipping_weight_unit;
480             $data->{DescriptionData}->{ShippingWeight} = {
481 0           unitOfMeasure => $unit,
482             _ => $ship_weight,
483             };
484             }
485              
486 0 0         if (my $product_data = $self->product_data) {
487 0           $data->{ProductData} = $product_data;
488             }
489 0           return $data;
490             }
491              
492             sub as_inventory_hash {
493 0     0 1   my $self = shift;
494 0 0         return unless $self->is_feed_needed('inventory');
495 0           my $quantity = $self->inventory;
496 0 0         if ($self->is_inactive) {
497 0           $quantity = 0;
498             }
499             return {
500 0           SKU => $self->sku,
501             Quantity => $quantity,
502             FulfillmentLatency => $self->ship_in_days,
503             };
504             }
505              
506             sub as_price_hash {
507 0     0 1   my $self = shift;
508 0 0         return unless $self->is_feed_needed('price');
509 0 0         return if $self->is_inactive;
510 0           my $price = $self->price;
511 0           my $data = {
512             SKU => $self->sku,
513             StandardPrice => { currency => $self->currency,
514             _ => sprintf('%.2f', $self->price) },
515             };
516 0 0         if ($self->sale_price) {
517 0 0 0       if ($self->sale_start && $self->sale_end) {
518             $data->{Sale} = {
519 0           SalePrice => { currency => $self->currency,
520             _ => sprintf('%.2f', $self->sale_price) },
521             StartDate => $self->sale_start->iso8601,
522             EndDate => $self->sale_end->iso8601,
523             };
524             }
525             else {
526 0           warn "Ignoring sale price, missing start or end date for "
527             . $self->sku . "\n";
528             }
529             }
530 0           return $data;
531             }
532              
533             =head2 as_images_array
534              
535             Return a data structure suitable to feed the ProductImage slot in a
536             Image feed.
537              
538             No output if the product is inactive.
539              
540             =over 4
541              
542             =item SKU
543              
544             =item ImageType The type of image (Main, Alternate, or Swatch)
545              
546             =over 4
547              
548             =item Main - Main image for the product
549              
550             =item Alternate (PT) - Other views of the product
551              
552             =item Swatch - Color or fabric (Note: Swatch images will be scaled down to 30 x 30 pixels
553             so they should only be used for displaying the color of your product's fabric, for
554             example, not for displaying your whole product.)
555              
556             =back
557              
558             =item ImageLocation
559              
560             The exact location of the image using a full URL (such as
561             http://mystore.com/images/1234.jpg). Amazon cannot access images
562             stored with a secured URL (https) so be sure to use http instead.
563              
564             =back
565              
566             =cut
567              
568             sub as_images_array {
569 0     0 1   my $self = shift;
570 0 0         return unless $self->is_feed_needed('image');
571 0 0         return if $self->is_inactive;
572 0 0         return unless $self->images;
573 0           my $sku = $self->sku;
574             # here we assign the first as the main one, the others as alternate.
575 0           my @images = @{ $self->images };
  0            
576 0           my @types = (qw/Main PT1 PT2 PT3 PT4 PT5 PT6 PT7 PT8/);
577              
578 0           my @out;
579              
580 0   0       while (@images && @types) {
581 0           my $img = shift @images;
582 0           my $type = shift @types;
583 0           push @out, {
584             SKU => $sku,
585             ImageType => $type,
586             ImageLocation => $img,
587             };
588             }
589 0 0         @out ? return \@out : return;
590             }
591              
592             =head2 as_variants_hash
593              
594             Return a structure suitable for the Relationship feed. No output if
595             the product is inactive.
596              
597             =cut
598              
599             sub as_variants_hash {
600 0     0 1   my $self = shift;
601 0 0         return unless $self->is_feed_needed('variants');
602 0 0         return if $self->is_inactive;
603 0           my $children = $self->children;
604 0 0 0       return unless $children && @$children;
605 0           my $data = { ParentSKU => $self->sku,
606             Relation => [] };
607 0           foreach my $child (@$children) {
608 0           push @{ $data->{Relation} }, {
  0            
609             SKU => $child,
610             Type => 'Variation',
611             };
612             }
613 0           return $data;
614             }
615              
616             =head2 condition_type_for_lowest_price_listing
617              
618             This is a method, not an accessor. Extract from the condition the
619             string needed by some API calls, where possible values are: New Used
620             Collectible Refurbished Club
621              
622             =cut
623              
624              
625              
626             sub condition_type_for_lowest_price_listing {
627 0     0 1   my $self = shift;
628 0           my $condition = $self->condition;
629 0 0         die "Shouldn't happen" unless $condition;
630             # beware the hack
631 0 0         if ($condition =~ m/^([A-Z][a-z]+)$/) {
    0          
632 0           return $condition;
633             }
634             elsif ($condition =~ m/^([A-Z][a-z]+)([A-Z][a-z]+)$/) {
635 0           return $1;
636             }
637             else {
638 0           die "$condition?";
639             }
640             }
641              
642              
643             1;