Amazon-MWS
view release on metacpan or search on metacpan
lib/Amazon/MWS/Uploader.pm view on Meta::CPAN
upload again previously failed orders.
=cut
has reset_all_errors => (is => 'ro');
=item reset_errors
A string containing a comma separated list of error codes, optionally
prefixed with a "!" (to reverse its meaning).
Example:
"!6024,6023"
Meaning: reupload all the products whose error code is B<not> 6024 or
6023.
"6024,6023"
Meaning: reupload the products whose error code was 6024 or 6023
=cut
has reset_errors => (is => 'ro',
isa => sub {
my $string = $_[0];
# undef/0/'' is fine
if ($string) {
die "reset_errors must be a comma separated list of error code, optionally prefixed by a '!' to negate its meaning"
if $string !~ m/^\s*!?\s*(([0-9]+)(\s*,\s*)?)+/;
}
});
has _reset_error_structure => (is => 'lazy');
sub _build__reset_error_structure {
my $self = shift;
my $reset_string = $self->reset_errors || '';
$reset_string =~ s/^\s*//;
$reset_string =~ s/\s*$//;
return unless $reset_string;
my $negate = 0;
if ($reset_string =~ m/^\s*!\s*(.+)/) {
$reset_string = $1;
$negate = 1;
}
my %codes = map { $_ => 1 } grep { $_ } split(/\s*,\s*/, $reset_string);
return unless %codes;
return {
negate => $negate,
codes => \%codes,
};
}
=item force
Same as above, but only for the selected items. An arrayref is
expected here with the B<skus>.
=cut
has force => (is => 'ro',
isa => ArrayRef,
);
has _force_hashref => (is => 'lazy');
sub _build__force_hashref {
my $self = shift;
my %forced;
if (my $arrayref = $self->force) {
%forced = map { $_ => 1 } @$arrayref;
}
return \%forced;
}
=item limit_inventory
If set to an integer, limit the inventory to this value. Setting this
to 0 will disable it.
=item job_hours_timeout
If set to an integer, abort the job after X hours are elapsed since
the job was started. Default to 3 hours. Set to 0 to disable (not
recommended).
This doesn't affect jobs for order acknowledgements (C<order_ack>), see below.
=item order_ack_days_timeout
Order acknowlegments times out at different rate, because it's somehow
sensitive.
=cut
has job_hours_timeout => (is => 'ro',
isa => Int,
default => sub { 3 });
has order_ack_days_timeout => (is => 'ro',
isa => Int,
default => sub { 30 });
has limit_inventory => (is => 'ro',
isa => Int);
=item schema_dir
The directory where the xsd files for the feed building can be found.
=item feeder
A L<Amazon::MWS::XML::Feed> object. Lazy attribute, you shouldn't pass
this to the constructor, it is lazily built using C<products>,
C<merchant_id> and C<schema_dir>.
lib/Amazon/MWS/Uploader.pm view on Meta::CPAN
Provided by Amazon.
=item secret_key
Provided by Amazon.
=item marketplace_id
L<http://docs.developer.amazonservices.com/en_US/dev_guide/DG_Endpoints.html>
=item endpoint
Ditto.
=cut
has merchant_id => (is => 'ro', required => 1);
has access_key_id => (is => 'ro', required => 1);
has secret_key => (is => 'ro', required => 1);
has marketplace_id => (is => 'ro', required => 1);
has endpoint => (is => 'ro', required => 1);
=item products
An arrayref of L<Amazon::MWS::XML::Product> objects, or anything that
(properly) responds to C<as_product_hash>, C<as_inventory_hash>,
C<as_price_hash>. See L<Amazon::MWS::XML::Product> for details.
B<This is set as read-write, so you can set the product after the
object construction, but if you change it afterward, you will get
unexpected results>.
This routine also check if the product needs upload and delete
disappeared products. If you are doing the check yourself, use
C<checked_products>.
=item checked_products
As C<products>, but no check is performed. This takes precedence.
=item sqla
Lazy attribute to hold the C<SQL::Abstract> object.
=cut
has products => (is => 'rw',
isa => ArrayRef);
has sqla => (
is => 'ro',
default => sub {
return SQL::Abstract->new;
}
);
has existing_products => (is => 'lazy');
sub _build_existing_products {
my $self = shift;
my $sth = $self->_exe_query($self->sqla->select(amazon_mws_products => [qw/sku
timestamp_string
status
listed
error_code
/],
{
status => { -not_in => [qw/deleted/] },
shop_id => $self->_unique_shop_id,
}));
my %uploaded;
while (my $row = $sth->fetchrow_hashref) {
$row->{timestamp_string} ||= 0;
$uploaded{$row->{sku}} = $row;
}
return \%uploaded;
}
has products_to_upload => (is => 'lazy');
has checked_products => (is => 'rw', isa => ArrayRef);
sub _build_products_to_upload {
my $self = shift;
if (my $checked = $self->checked_products) {
return $checked;
}
my $product_arrayref = $self->products;
return [] unless $product_arrayref && @$product_arrayref;
my @products = @$product_arrayref;
my $existing = $self->existing_products;
my @todo;
foreach my $product (@products) {
my $sku = $product->sku;
if (my $exists = $existing->{$sku}) {
# mark the item as visited
$exists->{_examined} = 1;
}
print "Checking $sku\n" if $self->debug;
next unless $self->product_needs_upload($product->sku, $product->timestamp_string);
print "Scheduling product " . $product->sku . " for upload\n";
if (my $limit = $self->limit_inventory) {
my $real = $product->inventory;
if ($real > $limit) {
print "Limiting the $sku inventory from $real to $limit\n" if $self->debug;
$product->inventory($limit);
}
}
if (my $children = $product->children) {
my @good_children;
foreach my $child (@$children) {
# skip failed children, but if the current status of
# parent is failed, and we reached this point, retry.
if (! exists $self->_force_hashref->{$child} and
$existing->{$child} and
$existing->{$sku} and
$existing->{$sku}->{status} ne 'failed' and
$existing->{$child}->{status} eq 'failed') {
print "Ignoring failed variant $child\n";
}
lib/Amazon/MWS/Uploader.pm view on Meta::CPAN
my ($self, $file) = @_;
open (my $fh, '<', $file) or die "Couldn't open $file $!";
local $/ = undef;
my $content = <$fh>;
close $fh;
return $content;
}
sub upload {
my $self = shift;
# create the feeds to be uploaded using the products
my @products = @{ $self->products_to_upload };
unless (@products) {
print "No products, can't upload anything\n";
return;
}
my $feeder = Amazon::MWS::XML::Feed->new(
products => \@products,
xml_writer => $self->xml_writer,
merchant_id => $self->merchant_id,
);
my @feeds;
foreach my $feed_name (qw/product
inventory
price
image
variants
/) {
my $method = $feed_name . "_feed";
if (my $content = $feeder->$method) {
push @feeds, {
name => $feed_name,
content => $content,
};
}
}
if (my $job_id = $self->prepare_feeds(upload => \@feeds)) {
$self->_mark_products_as_pending($job_id, @products);
return $job_id;
}
return;
}
sub _mark_products_as_pending {
my ($self, $job_id, @products) = @_;
die "Bad usage" unless $job_id;
# these skus were cleared up when asking for the products to upload
foreach my $p (@products) {
my %identifier = (
sku => $p->sku,
shop_id => $self->_unique_shop_id,
);
my %data = (
amws_job_id => $job_id,
status => 'pending',
warnings => '', # clear out
timestamp_string => $p->timestamp_string,
);
my $check = $self
->_exe_query($self->sqla->select(amazon_mws_products => [qw/sku/], { %identifier }));
my $existing = $check->fetchrow_hashref;
$check->finish;
if ($existing) {
$self->_exe_query($self->sqla->update(amazon_mws_products => \%data, \%identifier));
}
else {
$self->_exe_query($self->sqla->insert(amazon_mws_products => { %identifier, %data }));
}
}
}
sub prepare_feeds {
my ($self, $task, $feeds) = @_;
die "Missing task ($task) and feeds ($feeds)" unless $task && $feeds;
return unless @$feeds; # nothing to do
my $job_id = $task . "-" . DateTime->now->strftime('%F-%H-%M-%S');
my $job_started_epoch = time();
$self->_exe_query($self->sqla
->insert(amazon_mws_jobs => {
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
task => $task,
job_started_epoch => $job_started_epoch,
}));
# to complete the process, we need to fill out these five
# feeds. every feed has the same procedure, as per
# http://docs.developer.amazonservices.com/en_US/feeds/Feeds_Overview.html
# so we put a flag on the feed when it is done. The processing
# of the feed itself is tracked in the amazon_mws_feeds
# TODO: we could pass to the object some flags to filter out results.
foreach my $feed (@$feeds) {
# write out the feed if we got something to do, and add a row
# to the feeds.
# when there is no content, no need to create a job for it.
if (my $content = $feed->{content}) {
my $name = $feed->{name} or die "Missing feed_name";
my $file = $self->_feed_file_for_method($job_id, $name);
open (my $fh, '>', $file) or die "Couldn't open $file $!";
print $fh $content;
close $fh;
# and prepare a row for it
my $insertion = {
feed_name => $name,
feed_file => $file,
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
};
$self->_exe_query($self->sqla
->insert(amazon_mws_feeds => $insertion));
}
}
return $job_id;
}
sub get_pending_jobs {
my ($self, @args) = @_;
my %additional;
my @named_jobs;
foreach my $arg (@args) {
if (!ref($arg)) {
push @named_jobs, $arg;
}
elsif (ref($arg) eq 'HASH') {
# add the filters
foreach my $key (keys %$arg) {
if ($additional{$key}) {
die "Attempt to overwrite $key in the additional parameters!\n";
}
else {
$additional{$key} = $arg->{$key};
}
}
}
else {
die "Argument must be either a scalar with a job name and/or "
. "an hashref with additional filters!";
}
}
if (@named_jobs) {
$additional{amws_job_id} = { -in => \@named_jobs };
}
my ($stmt, @bind) = $self->sqla->select(amazon_mws_jobs => '*',
{
%additional,
aborted => 0,
success => 0,
shop_id => $self->_unique_shop_id,
},
{ -asc => 'job_started_epoch'});
my $pending = $self->_exe_query($stmt, @bind);
my %jobs;
while (my $row = $pending->fetchrow_hashref) {
$jobs{$row->{task}} ||= [];
push @{$jobs{$row->{task}}}, $row;
}
my @out;
foreach my $task (qw/product_deletion upload shipping_confirmation order_ack/) {
if (my $list = delete $jobs{$task}) {
if ($task eq 'order_ack') {
for (1..2) {
push @out, pop @$list if @$list;
}
}
elsif ($task eq 'shipping_confirmation') {
while (@$list) {
push @out, pop @$list;
}
}
else {
push @out, @$list if @$list;
}
}
}
return @out;
}
sub resume {
my ($self, @args) = @_;
foreach my $row ($self->get_pending_jobs(@args)) {
print "Working on $row->{amws_job_id}\n";
# check if the job dir exists
if (-d $self->_feed_job_dir($row->{amws_job_id})) {
if (my $seconds_elapsed = $self->job_timed_out($row)) {
$self->_print_or_warn_error("Timeout reached for $row->{amws_job_id}, aborting: "
. Dumper($row));
$self->cancel_job($row->{task}, $row->{amws_job_id},
"Job timed out after $seconds_elapsed seconds");
next;
}
$self->process_feeds($row);
}
else {
warn "No directory " . $self->_feed_job_dir($row->{amws_job_id}) .
" found, removing job id $row->{amws_job_id}\n";
$self->cancel_job($row->{task}, $row->{amws_job_id},
"Job canceled due to missing feed directory");
}
}
}
=head2 cancel_job($task, $job_id, $reason)
Abort the job setting the aborted flag in C<amazon_mws_jobs> table.
=cut
sub cancel_job {
my ($self, $task, $job_id, $reason) = @_;
$self->_exe_query($self->sqla->update('amazon_mws_jobs',
{
aborted => 1,
status => $reason,
},
{
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
# and revert the products' status
my $status;
if ($task eq 'product_deletion') {
# let's pretend we were deleting good products
$status = 'ok';
}
elsif ($task eq 'upload') {
$status = 'redo';
}
if ($status) {
print "Updating product to $status for products with job id $job_id\n";
$self->_exe_query($self->sqla->update('amazon_mws_products',
{ status => $status },
{
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
}
}
=head2 process_feeds(\%job_row)
Given the hashref with the db row of the job, check at which point it
is and resume.
=cut
sub process_feeds {
my ($self, $row) = @_;
# print Dumper($row);
# upload the feeds one by one and stop if something is blocking
my $job_id = $row->{amws_job_id};
print "Processing job $job_id\n";
# query the feeds table for this job
my ($stmt, @bind) = $self->sqla->select(amazon_mws_feeds => '*',
{
amws_job_id => $job_id,
aborted => 0,
success => 0,
shop_id => $self->_unique_shop_id,
},
['amws_feed_pk']);
my $sth = $self->_exe_query($stmt, @bind);
my $unfinished;
while (my $feed = $sth->fetchrow_hashref) {
last unless $self->upload_feed($feed);
}
$sth->finish;
($stmt, @bind) = $self->sqla->select(amazon_mws_feeds => '*',
{
shop_id => $self->_unique_shop_id,
amws_job_id => $job_id,
});
$sth = $self->_exe_query($stmt, @bind);
my ($total, $success, $aborted) = (0, 0, 0);
# query again and check if we have aborted jobs;
while (my $feed = $sth->fetchrow_hashref) {
$total++;
$success++ if $feed->{success};
$aborted++ if $feed->{aborted};
}
# a job was aborted
my $update;
if ($aborted) {
$update = {
aborted => 1,
status => 'Feed error',
};
$self->_print_or_warn_error("Job $job_id aborted!\n");
}
elsif ($success == $total) {
$update = { success => 1 };
print "Job successful!\n";
# if we're here, all the products are fine, so mark them as
# such if it's an upload job
if ($row->{task} eq 'upload') {
$self->_exe_query($self->sqla->update('amazon_mws_products',
{ status => 'ok',
listed_date => DateTime->now,
listed => 1,
},
{
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
}
}
else {
print "Job still to be processed\n";
}
if ($update) {
$self->_exe_query($self->sqla->update(amazon_mws_jobs => $update,
{
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
}
}
=head2 upload_feed($type, $feed_id);
Routine to upload the feed. Return true if it's complete, false
otherwise.
=cut
lib/Amazon/MWS/Uploader.pm view on Meta::CPAN
}
catch {
die Dumper($_);
};
}
my @orders;
foreach my $order (@order_structs) {
my $amws_id = $order->{AmazonOrderId};
die "Missing amazon AmazonOrderId?!" unless $amws_id;
my $get_orderline = sub {
# begin of the closure
my $orderline;
my @items;
try {
$orderline = $self->client->ListOrderItems(AmazonOrderId => $amws_id);
push @items, @{ $orderline->{OrderItems}->{OrderItem} };
}
catch {
my $err = $_;
if (blessed($err) && $err->isa('Amazon::MWS::Exception::Throttled')) {
die "Request is throttled. Consider to adjust order_days_range as documented at https://metacpan.org/pod/Amazon::MWS::Uploader#ACCESSORS";
}
else {
die Dumper($err);
}
};
while (my $next = $orderline->{NextToken}) {
try {
$orderline =
$self->client->ListOrderItemsByNextToken(NextToken => $next);
push @items, @{ $orderline->{OrderItems}->{OrderItem} };
}
catch {
die Dumper($_);
};
}
return \@items;
# end of the closure
};
push @orders, Amazon::MWS::XML::Order->new(order => $order,
retrieve_orderline_sub => $get_orderline);
}
return @orders;
}
=head2 order_already_registered($order)
Check in the amazon_mws_orders table if we already registered this
order.
Return the row for this table (as an hashref) if present, nothing
underwise.
=cut
sub order_already_registered {
my ($self, $order) = @_;
die "Bad usage, missing order" unless $order;
my $sth = $self->_exe_query($self->sqla->select(amazon_mws_orders => '*',
{
amazon_order_id => $order->amazon_order_number,
shop_id => $self->_unique_shop_id,
}));
if (my $exists = $sth->fetchrow_hashref) {
$sth->finish;
return $exists;
}
else {
return;
}
}
=head2 acknowledge_successful_order(@orders)
Accept a list of L<Amazon::MWS::XML::Order> objects, prepare a
acknowledge feed with the C<Success> status, and insert the orders in
the database.
=cut
sub acknowledge_successful_order {
my ($self, @orders) = @_;
my @orders_to_register;
foreach my $ord (@orders) {
if (my $existing = $self->order_already_registered($ord)) {
if ($existing->{confirmed}) {
print "Skipping already confirmed order $existing->{amazon_order_id} => $existing->{shop_order_id}\n";
}
else {
# it's not complete, so print out diagnostics
warn "Order $existing->{amazon_order_id} uncompletely registered with id $existing->{shop_order_id}, please indagate why (skipping)\n" . Dumper($existing);
}
}
else {
push @orders_to_register, $ord;
}
}
return unless @orders_to_register;
my $feed_content = $self->acknowledge_feed(Success => @orders_to_register);
# here we have only one feed to upload and check
my $job_id = $self->prepare_feeds(order_ack => [{
name => 'order_ack',
content => $feed_content,
}]);
# store the pairing amazon order id / shop order id in our table
foreach my $order (@orders_to_register) {
my %order_pairs = (
shop_id => $self->_unique_shop_id,
amazon_order_id => $order->amazon_order_number,
# this will die if we try to insert an undef order_number
shop_order_id => $order->order_number,
amws_job_id => $job_id,
);
$self->_exe_query($self->sqla->insert(amazon_mws_orders => \%order_pairs));
}
}
=head2 acknowledge_feed($status, @orders)
The first argument is usually C<Success>. The other arguments is a
list of L<Amazon::MWS::XML::Order> objects.
=cut
sub acknowledge_feed {
my ($self, $status, @orders) = @_;
die "Missing status" unless $status;
die "Missing orders" unless @orders;
my $feeder = $self->generic_feeder;
my $counter = 1;
my @messages;
foreach my $order (@orders) {
my $data = $order->as_ack_order_hashref;
$data->{StatusCode} = $status;
push @messages, {
MessageID => $counter++,
OrderAcknowledgement => $data,
};
}
return $feeder->create_feed(OrderAcknowledgement => \@messages);
}
=head2 delete_skus(@skus)
Accept a list of skus. Prepare a C<product_deletion> feed and update
the database.
=cut
sub delete_skus {
my ($self, @skus) = @_;
return unless @skus;
print "Trying to purge missing items " . join(" ", @skus) . "\n";
# delete only products which are not in pending status
my $check = $self
->_exe_query($self->sqla
->select('amazon_mws_products', [qw/sku status/],
{
sku => { -in => \@skus },
shop_id => $self->_unique_shop_id,
}));
my %our_skus;
while (my $p = $check->fetchrow_hashref) {
$our_skus{$p->{sku}} = $p->{status};
}
my @checked;
while (@skus) {
my $sku = shift @skus;
if (my $status = $our_skus{$sku}) {
if ($status eq 'pending' or
$status eq 'deleted') {
print "Skipping $sku deletion, in status $status\n";
next;
}
}
else {
warn "$sku not found in amazon_mws_products, deleting anyway\n";
}
push @checked, $sku;
}
@skus = @checked;
unless (@skus) {
print "Not purging anything\n";
return;
}
print "Actually purging items " . join(" ", @skus) . "\n";
my $feed_content = $self->delete_skus_feed(@skus);
my $job_id = $self->prepare_feeds(product_deletion => [{
name => 'product',
content => $feed_content,
}] );
# delete the skus locally
$self->_exe_query($self->sqla->update('amazon_mws_products',
{
status => 'deleted',
amws_job_id => $job_id,
},
{
sku => { -in => \@skus },
shop_id => $self->_unique_shop_id,
}));
}
=head2 delete_skus_feed(@skus)
Prepare a feed (via C<create_feed>) to delete the given skus.
=cut
sub delete_skus_feed {
my ($self, @skus) = @_;
return unless @skus;
my $feeder = $self->generic_feeder;
my $counter = 1;
my @messages;
foreach my $sku (@skus) {
push @messages, {
MessageID => $counter++,
OperationType => 'Delete',
Product => {
SKU => $sku,
}
};
}
return $feeder->create_feed(Product => \@messages);
}
sub register_order_ack_errors {
my ($self, $job_id, $result) = @_;
my @errors = $result->report_errors;
# we hope to have just one error, but in case...
my %update;
if (@errors > 1) {
my @errors_with_code = grep { $_->{code} } @errors;
my $error_code = AMW_ORDER_WILDCARD_ERROR;
if (@errors_with_code) {
# pick just the first, the field is an integer
$error_code = $errors_with_code[0]{code};
}
my $error_msgs = join('\n', map { $_->{type} . ' ' . $_->{message} . ' ' . $_->{code} } @errors);
%update = (
error_msg => $error_msgs,
error_code => $error_code,
);
}
elsif (@errors) {
my $error = shift @errors;
%update = (
error_msg => $error->{type} . ' ' . $error->{message} . ' ' . $error->{code},
error_code => $error->{code},
);
}
else {
%update = (
error_msg => $result->errors,
error_code => AMW_ORDER_WILDCARD_ERROR,
);
}
if (%update) {
$self->_exe_query($self->sqla->update('amazon_mws_orders',
\%update,
{ amws_job_id => $job_id,
shop_id => $self->_unique_shop_id }));
}
else {
warn "register_order_ack_errors couldn't parse " . Dumper($result);
}
# then get the amazon order number and recheck
my $sth = $self->_exe_query($self->sqla->select('amazon_mws_orders',
[qw/amazon_order_id
shop_order_id
/],
{
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
my ($amw_order_number, $shop_order_id) = $sth->fetchrow_array;
if ($sth->fetchrow_array) {
warn "Multiple jobs found for $job_id in amazon_mws_orders!";
}
$sth->finish;
if (my $status = $self->update_amw_order_status($amw_order_number)) {
warn "$amw_order_number ($shop_order_id) has now status $status!\n";
}
}
sub register_ship_order_errors {
my ($self, $job_id, $result) = @_;
# here we get the amazon ids,
my @orders = $self->orders_in_shipping_job($job_id);
my $errors = $result->orders_errors;
# filter
my @errors_with_order = grep { $_->{order_id} } @$errors;
my %errs = map { $_->{order_id} => {job_id => $job_id, code => $_->{code}, error => $_->{error}} } @errors_with_order;
foreach my $ord (@orders) {
if (my $fault = $errs{$ord}) {
$self->_exe_query($self->sqla->update('amazon_mws_orders',
{
shipping_confirmation_error => "$fault->{code} $fault->{error}",
},
{
amazon_order_id => $ord,
shipping_confirmation_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
}
else {
# this looks good
$self->_exe_query($self->sqla->update('amazon_mws_orders',
{
shipping_confirmation_ok => 1,
},
{
amazon_order_id => $ord,
shipping_confirmation_job_id => $job_id,
shop_id => $self->_unique_shop_id
}));
}
}
}
=head2 register_errors($job_id, $result)
The first argument is the job ID. The second is a
L<Amazon::MWS::XML::Response::FeedSubmissionResult> object.
This method will update the status of the products (either C<failed>
lib/Amazon/MWS/Uploader.pm view on Meta::CPAN
=head2 register_order_ack_errors($job_id, $result);
Same arguments as above, but for order acknowledgements.
=head2 register_ship_order_errors($job_id, $result);
Same arguments as above, but for shipping notifications.
=cut
sub register_errors {
my ($self, $job_id, $result) = @_;
# first, get the list of all the skus which were scheduled for this job
# we don't have a products hashref anymore.
# probably we could parse back the produced xml, but looks like an overkill.
# just mark them as redo and wait for the next cron call.
my @products = $self->skus_in_job($job_id);
my $errors = $result->skus_errors;
my @errors_with_sku = grep { $_->{sku} } @$errors;
# turn it into an hash
my %errs = map { $_->{sku} => {job_id => $job_id, code => $_->{code}, error => $_->{error}} } @errors_with_sku;
foreach my $sku (@products) {
if ($errs{$sku}) {
$self->_exe_query($self->sqla->update('amazon_mws_products',
{
status => 'failed',
error_code => $errs{$sku}->{code},
error_msg => "$errs{$sku}->{job_id} $errs{$sku}->{code} $errs{$sku}->{error}",
},
{
sku => $sku,
shop_id => $self->_unique_shop_id,
}));
}
else {
# this is good, mark it to be redone
$self->_exe_query($self->sqla->update('amazon_mws_products',
{
status => 'redo',
},
{
sku => $sku,
shop_id => $self->_unique_shop_id,
}));
print "Scheduling $sku for redoing\n";
}
}
}
=head2 skus_in_job($job_id)
Check the amazon_mws_product for the SKU which were uploaded by the
given job ID.
=cut
sub skus_in_job {
my ($self, $job_id) = @_;
my $sth = $self->_exe_query($self->sqla->select('amazon_mws_products',
[qw/sku/],
{
amws_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
my @skus;
while (my $row = $sth->fetchrow_hashref) {
push @skus, $row->{sku};
}
return @skus;
}
=head2 get_asin_for_eans(@eans)
Accept a list of EANs and return an hashref where the keys are the
eans passed as arguments, and the values are the ASIN for the current
marketplace. Max EANs: 5.x
http://docs.developer.amazonservices.com/en_US/products/Products_GetMatchingProductForId.html
=head2 get_asin_for_skus(@skus)
Same as above (with the same limit of 5 items), but for SKUs.
=head2 get_asin_for_sku($sku)
Same as above, but for a single sku. Return the ASIN or undef if not
found.
=head2 get_asin_for_ean($ean)
Same as above, but for a single ean. Return the ASIN or undef if not
found.
=cut
sub get_asin_for_skus {
my ($self, @skus) = @_;
return $self->_get_asin_for_type(SellerSKU => @skus);
}
sub get_asin_for_eans {
my ($self, @eans) = @_;
return $self->_get_asin_for_type(EAN => @eans);
}
sub _get_asin_for_type {
my ($self, $type, @list) = @_;
die "Max 5 products to get the asin for $type!" if @list > 5;
my $client = $self->client;
my $res;
try {
$res = $client->GetMatchingProductForId(IdType => $type,
IdList => \@list,
MarketplaceId => $self->marketplace_id);
}
catch { die Dumper($_) };
my %ids;
if ($res && @$res) {
foreach my $product (@$res) {
lib/Amazon/MWS/Uploader.pm view on Meta::CPAN
ExcludeMe => 1,
ItemCondition => $condition || 'New',
);
}
catch { die Dumper($_) };
return unless $listing && @$listing;
my $lowest;
foreach my $item (@$listing) {
my $current = $item->{Price}->{LandedPrice}->{Amount};
$lowest ||= $current;
if ($current < $lowest) {
$lowest = $current;
}
}
return $lowest;
}
=head2 shipping_confirmation_feed(@shipped_orders)
Return a feed string with the shipping confirmation. A list of
L<Amazon::MWS::XML::ShippedOrder> object must be passed.
=cut
sub shipping_confirmation_feed {
my ($self, @shipped_orders) = @_;
die "Missing Amazon::MWS::XML::ShippedOrder argument" unless @shipped_orders;
my $feeder = $self->generic_feeder;
my $counter = 1;
my @messages;
foreach my $order (@shipped_orders) {
push @messages, {
MessageID => $counter++,
OrderFulfillment => $order->as_shipping_confirmation_hashref,
};
}
return $feeder->create_feed(OrderFulfillment => \@messages);
}
=head2 send_shipping_confirmation($shipped_orders)
Schedule the shipped orders (an L<Amazon::MWS::XML::ShippedOrder>
object) for the uploading.
=head2 order_already_shipped($shipped_order)
Check if the shipped orders (an L<Amazon::MWS::XML::ShippedOrder> was
already notified as shipped looking into our table, returning the row
with the order.
To see the status, check shipping_confirmation_ok (already done),
shipping_confirmation_error (faulty), shipping_confirmation_job_id (pending).
=cut
sub order_already_shipped {
my ($self, $order) = @_;
my $condition = $self->_condition_for_shipped_orders($order);
my $sth = $self->_exe_query($self->sqla->select(amazon_mws_orders => '*', $condition));
if (my $row = $sth->fetchrow_hashref) {
die "Multiple results found in amazon_mws_orders for " . Dumper($condition)
if $sth->fetchrow_hashref;
return $row;
}
else {
return;
}
}
sub send_shipping_confirmation {
my ($self, @orders) = @_;
my @orders_to_notify;
foreach my $ord (@orders) {
if (my $report = $self->order_already_shipped($ord)) {
if ($report->{shipping_confirmation_ok}) {
print "Skipping ship-confirm for order $report->{amazon_order_id} $report->{shop_order_id}: already notified\n";
}
elsif (my $error = $report->{shipping_confirmation_error}) {
if ($self->reset_all_errors) {
warn "Submitting again previously failed job $report->{amazon_order_id} $report->{shop_order_id}\n";
push @orders_to_notify, $ord;
}
else {
warn "Skipping ship-confirm for order $report->{amazon_order_id} $report->{shop_order_id} with error $error\n";
}
}
elsif ($report->{shipping_confirmation_job_id}) {
print "Skipping ship-confirm for order $report->{amazon_order_id} $report->{shop_order_id}: pending\n";
}
else {
push @orders_to_notify, $ord;
}
}
else {
die "It looks like you are trying to send a shipping confirmation "
. " without prior order acknowlegdement. "
. "At least in the amazon_mws_orders there is no trace of "
. "$report->{amazon_order_id} $report->{shop_order_id}";
}
}
return unless @orders_to_notify;
my $feed_content = $self->shipping_confirmation_feed(@orders_to_notify);
# here we have only one feed to upload and check
my $job_id = $self->prepare_feeds(shipping_confirmation => [{
name => 'shipping_confirmation',
content => $feed_content,
}]);
# and store the job id in the table
foreach my $ord (@orders_to_notify) {
$self->_exe_query($self->sqla->update(amazon_mws_orders => {
shipping_confirmation_job_id => $job_id,
shipping_confirmation_error => undef,
},
$self->_condition_for_shipped_orders($ord)));
}
}
sub _condition_for_shipped_orders {
my ($self, $order) = @_;
die "Missing order" unless $order;
my %condition = (shop_id => $self->_unique_shop_id);
if (my $amazon_order_id = $order->amazon_order_id) {
$condition{amazon_order_id} = $amazon_order_id;
}
elsif (my $order_id = $order->merchant_order_id) {
$condition{shop_order_id} = $order_id;
}
else {
die "Missing amazon_order_id or merchant_order_id";
}
return \%condition;
}
=head2 orders_waiting_for_shipping
Return a list of hashref with two keys, C<amazon_order_id> and
C<shop_order_id> for each order which is waiting confirmation.
This is implemented looking into amazon_mws_orders where there is no
shipping confirmation job id.
The confirmed flag (which means we acknowledged the order) is ignored
to avoid stuck order_ack jobs to prevent the shipping confirmation.
=cut
sub orders_waiting_for_shipping {
my $self = shift;
my $sth = $self->_exe_query($self->sqla->select('amazon_mws_orders',
[qw/amazon_order_id
shop_order_id/],
{
shop_id => $self->_unique_shop_id,
shipping_confirmation_job_id => undef,
# do not stop the unconfirmed to be considered
# confirmed => 1,
}));
my @out;
while (my $row = $sth->fetchrow_hashref) {
push @out, $row;
}
return @out;
}
=head2 product_needs_upload($sku, $timestamp)
Lookup the product $sku with timestamp $timestamp and return the sku
if the product needs to be uploaded or can be safely skipped. This
method is stateless and doesn't alter anything.
=cut
sub product_needs_upload {
my ($self, $sku, $timestamp) = @_;
my $debug = $self->debug;
return unless $sku;
my $forced = $self->_force_hashref;
# if it's forced, we have nothing to check, just pass it.
if ($forced->{$sku}) {
print "Forcing $sku as requested\n" if $debug;
return $sku;
}
$timestamp ||= 0;
my $existing = $self->existing_products;
if (exists $existing->{$sku}) {
if (my $exists = $existing->{$sku}) {
my $status = $exists->{status} || '';
if ($status eq 'ok') {
if ($exists->{timestamp_string} eq $timestamp) {
return;
}
else {
return $sku;
}
}
elsif ($status eq 'redo') {
return $sku;
}
elsif ($status eq 'failed') {
if ($self->reset_all_errors) {
return $sku;
}
elsif (my $reset = $self->_reset_error_structure) {
# option for this error was passed.
my $error = $exists->{error_code};
my $match = $reset->{codes}->{$error};
if (($match && $reset->{negate}) or
(!$match && !$reset->{negate})) {
# was passed !this error or !random , so do not reset
print "Skipping failed item $sku with error code $error\n" if $debug;
return;
}
else {
# otherwise reset
print "Resetting error for $sku with error code $error\n" if $debug;
return $sku;
}
}
else {
print "Skipping failed item $sku\n" if $debug;
return;
}
}
elsif ($status eq 'pending') {
print "Skipping pending item $sku\n" if $debug;
return;
}
die "I shouldn't have reached this point with status <$status>";
}
}
print "$sku wasn't uploaded so far, scheduling it\n" if $debug;
return $sku;
}
=head2 orders_in_shipping_job($job_id)
Lookup the C<amazon_mws_orders> table and return a list of
C<amazon_order_id> for the given shipping confirmation job. INTERNAL.
=cut
sub orders_in_shipping_job {
my ($self, $job_id) = @_;
die unless $job_id;
my $sth = $self->_exe_query($self->sqla->select(amazon_mws_orders => [qw/amazon_order_id/],
{
shipping_confirmation_job_id => $job_id,
shop_id => $self->_unique_shop_id,
}));
my @orders;
while (my $row = $sth->fetchrow_hashref) {
push @orders, $row->{amazon_order_id};
}
return @orders;
}
=head2 put_product_on_error(sku => $sku, timestamp_string => $timestamp, error_code => $error_code, error_msg => $error)
Register a custom error for the product $sku with error $error and
$timestamp as the timestamp string. The error is optional, and will be
"shop error" if not provided. The error code will be 1 if not provided.
=cut
sub put_product_on_error {
my ($self, %product) = @_;
die "Missing sku" unless $product{sku};
die "Missing timestamp" unless defined $product{timestamp_string};
my %identifier = (
shop_id => $self->_unique_shop_id,
sku => $product{sku},
);
my %errors = (
status => 'failed',
error_msg => $product{error_msg} || 'shop error',
error_code => $product{error_code} || 1,
timestamp_string => $product{timestamp_string},
);
# check if we have it
my $sth = $self->_exe_query($self->sqla
->select('amazon_mws_products',
[qw/sku/], { %identifier }));
if ($sth->fetchrow_hashref) {
$sth->finish;
print "Updating $product{sku} with error $product{error_msg}\n";
$self->_exe_query($self->sqla->update('amazon_mws_products',
\%errors, \%identifier));
}
else {
print "Inserting $identifier{sku} with error $errors{error_msg}\n";
$self->_exe_query($self->sqla
->insert('amazon_mws_products',
{
%identifier,
%errors,
}));
}
}
=head2 cancel_feed($feed_id)
Call the CancelFeedSubmissions API and abort the feed and the
belonging job if found in the list. Return the response, which
probably is not even meaningful. It is a big FeedSubmissionInfo with
the past feed submissions.
=cut
sub cancel_feed {
my ($self, $feed) = @_;
die "Missing feed id argument" unless $feed;
# do the api call
my $sth = $self->_exe_query($self->sqla
->select(amazon_mws_feeds => [qw/amws_job_id/],
{
shop_id => $self->_unique_shop_id,
feed_id => $feed,
aborted => 0,
success => 0,
processing_complete => 0,
}));
my $feed_record = $sth->fetchrow_hashref;
if ($feed_record) {
$sth->finish;
print "Found $feed in pending state\n";
# abort it on our side
$self->_exe_query($self->sqla
->update('amazon_mws_feeds',
{
aborted => 1,
errors => "Canceled by shop action",
},
{
feed_id => $feed,
shop_id => $self->_unique_shop_id,
}));
# and abort the job as well
$self->_exe_query($self->sqla
->update('amazon_mws_jobs',
{
aborted => 1,
status => "Job aborted by cancel_feed",
},
{
amws_job_id => $feed_record->{amws_job_id},
shop_id => $self->_unique_shop_id,
}));
# and set the belonging products to redo
$self->_exe_query($self->sqla
->update('amazon_mws_products',
{
status => 'redo',
},
{
amws_job_id => $feed_record->{amws_job_id},
shop_id => $self->_unique_shop_id,
}));
}
else {
warn "No $feed found in pending list, trying to cancel anyway\n";
}
return $self->client->CancelFeedSubmissions(IdList => [ $feed ]);
}
sub _error_logger {
my ($self, $error_or_warning, $error_code, $message) = @_;
my $mode = 'warn';
my $modes = $self->skus_warnings_modes;
my $out_message = "$error_or_warning: $message ($error_code)\n";
# hardcode 8008 as print
$modes->{8008} = 'print';
if (exists $modes->{$error_code}) {
$mode = $modes->{$error_code};
}
if ($mode eq 'print') {
print $out_message;
}
elsif ($mode eq 'warn') {
warn $out_message;
}
elsif ($mode eq 'skip') {
# do nothing
}
else {
warn "Invalid mode $mode for $out_message";
}
}
=head2 update_amw_order_status($amazon_order_number)
Check the order status on Amazon and update the row in the
amazon_mws_orders table.
=cut
sub update_amw_order_status {
my ($self, $order) = @_;
# first, check if it exists
return unless $order;
my $sth = $self->_exe_query($self->sqla->select('amazon_mws_orders',
'*',
{
amazon_order_id => $order,
shop_id => $self->_unique_shop_id,
}));
my $order_ref = $sth->fetchrow_hashref;
die "Multiple rows found for $order!" if $sth->fetchrow_hashref;
print Dumper($order_ref);
my $res = $self->client->GetOrder(AmazonOrderId => [$order]);
my $amazon_order;
if ($res && $res->{Orders}->{Order} && @{$res->{Orders}->{Order}}) {
if (@{$res->{Orders}->{Order}} > 1) {
warn "Multiple results for order $order: " . Dumper($res);
}
$amazon_order = $res->{Orders}->{Order}->[0];
}
else {
warn "Order $order not found on amazon!"
}
print Dumper($amazon_order);
my $obj = Amazon::MWS::XML::Order->new(order => $amazon_order);
my $status = $obj->order_status;
$self->_exe_query($self->sqla->update('amazon_mws_orders',
{ status => $status },
{
amazon_order_id => $order,
shop_id => $self->_unique_shop_id,
}));
return $status;
}
=head2 get_products_with_error_code(@error_codes)
Return a list of hashref with the rows from C<amazon_mws_products> for
the current shop and the error code passed as argument. If no error
codes are passed, fetch all the products in error.
=head2 get_products_with_warnings
Returns a list of hashref, with C<sku> and C<warnings> as keys, for
each product in the shop which has the warnings set to something.
=cut
sub get_products_with_error_code {
my ($self, @errcodes) = @_;
my $where = { '>' => 0 };
if (@errcodes) {
$where = { -in => \@errcodes };
}
my $sth = $self->_exe_query($self->sqla
->select('amazon_mws_products', '*',
{
status => { '!=' => 'deleted' },
shop_id => $self->_unique_shop_id,
error_code => $where,
},
[qw/error_code sku/]));
my @records;
while (my $row = $sth->fetchrow_hashref) {
push @records, $row;
}
return @records;
}
sub get_products_with_warnings {
my $self = shift;
my $sth = $self->_exe_query($self->sqla
->select('amazon_mws_products', '*',
{
status => 'ok',
shop_id => $self->_unique_shop_id,
warnings => { '!=' => '' },
},
[qw/sku warnings/]));
my @records;
while (my $row = $sth->fetchrow_hashref) {
push @records, $row;
}
return @records;
}
=head2 mark_failed_products_as_redo(@skus)
Alter the status of the failed skus passed as argument from 'failed'
to 'redo' to trigger an update.
=cut
sub mark_failed_products_as_redo {
my ($self, @skus) = @_;
return unless @skus;
$self->_exe_query($self->sqla->update('amazon_mws_products',
{
status => 'redo',
},
{
shop_id => $self->_unique_shop_id,
status => 'failed',
sku => { -in => \@skus },
}));
}
=head2 get_products_with_amazon_shop_mismatches(@errors)
Parse the amazon_mws_products and return an hashref where the keys are
the failed skus, and the values are hashrefs where the keys are the
mismatched fields and the values are hashrefs with these keys:
Mismatched fields may be: C<part_number>, C<title>, C<manufacturer>,
C<brand>, C<color>, C<size>
=over 4
=item shop
The value on the shop
=item amazon
The value of the amazon product
=item error_code
The error code
=back
E.g.
lib/Amazon/MWS/Uploader.pm view on Meta::CPAN
}
die "Something is off, timeout not defined" unless defined $timeout;
return unless $timeout;
my $elapsed = $now - $started;
if ($elapsed > $timeout) {
return $elapsed;
}
else {
return;
}
}
sub _print_or_warn_error {
my ($self, @args) = @_;
my $action;
if (@args) {
if ($self->quiet) {
$action = 'print';
print @args;
}
else {
$action = 'warn';
warn @args;
}
}
return ($action, @args);
}
=head2 purge_old_jobs($limit)
Eventually the jobs and feed tables grow and never get purged. You can
call this method to remove from the db all the feeds older than
C<order_ack_days_timeout> (30 by default).
To avoid too much load on the db, you can set the limit to purge the
jobs. Defaults to 500. Set it to 0 to disable it.
=cut
sub purge_old_jobs {
my ($self, $limit) = @_;
unless (defined $limit) {
$limit = 500;
}
my $range = time() - $self->order_ack_days_timeout * 60 * 60 * 24;
my @and = (
task => [qw/product_deletion
upload/],
job_started_epoch => { '<', $range },
[ -or => {
aborted => 1,
success => 1,
},
],
);
if (my $shop_id = $self->shop_id) {
push @and, shop_id => $shop_id;
}
my $sth = $self->_exe_query($self->sqla
->select(amazon_mws_jobs => [qw/amws_job_id shop_id/],
[ -and => \@and ] ));
my @purge_jobs;
my $count = 0;
while (my $where = $sth->fetchrow_hashref) {
if ($limit) {
last if $count++ > $limit;
}
push @purge_jobs, $where;
}
$sth->finish;
if (@purge_jobs) {
$self->_exe_query($self->sqla->delete(amazon_mws_feeds => \@purge_jobs));
$self->_exe_query($self->sqla->delete(amazon_mws_jobs => \@purge_jobs));
while (@purge_jobs) {
my $feed = shift @purge_jobs;
my $dir = path($self->feed_dir)->child($feed->{shop_id}, $feed->{amws_job_id});
if ($dir->exists) {
print "Removing " . $dir->canonpath . "\n"; # unless $self->quiet;
$dir->remove_tree;
}
else {
print "$dir doesn't exist\n";
}
}
}
else {
print "Nothing to purge\n" unless $self->quiet;
}
}
1;
( run in 1.577 second using v1.01-cache-2.11-cpan-39bf76dae61 )