Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send stock (inventory) to Amazon #829

Open
jordan26 opened this issue Dec 6, 2024 · 6 comments
Open

Send stock (inventory) to Amazon #829

jordan26 opened this issue Dec 6, 2024 · 6 comments

Comments

@jordan26
Copy link

jordan26 commented Dec 6, 2024

Problem description:

My 'problem' is just the fact I have yet to see any method how to send stock (inventory) updates to Amazon. I used to have a custom function handling this when using v5 of the SDK. However since v7 I have no idea where to even start.

Here is a copy of my original approach when using v5 in case this is of use to others too. I am not using this any more, just to note. Unless someone knows how to use this whilst still on v7 too by chance.

/**
 * Use the jlevers/selling-partner-api library to send stock updates to Amazon. Build the XML file and send it as POST_INVENTORY_AVAILABILITY_DATA feed type.
 *
 * @param array $data
 */

public function send_stock_to_amazon( $data ) {

    // If environment is development or staging, don't send stock to Amazon.
    if( WP_ENV == 'development' || WP_ENV == 'staging' ) {
        return;
    }

    $config = new \SellingPartnerApi\Configuration([
        "lwaClientId" => LWA_CLIENT_ID,
        "lwaClientSecret" => LWA_CLIENT_SECRET,
        "lwaRefreshToken" => LWA_REFRESH_TOKEN,
        "awsAccessKeyId" => AWS_ACCESS_KEY_ID,
        "awsSecretAccessKey" => AWS_SECRET_ACCESS_KEY,
        "endpoint" => \SellingPartnerApi\Endpoint::EU,
    ]);

    $feedType = FeedType::POST_INVENTORY_AVAILABILITY_DATA;
    $feedsApi = new FeedsApi($config);

    // Create feed document
    $createFeedDocSpec = new Feeds\CreateFeedDocumentSpecification(['content_type' => $feedType['contentType']]);
    $feedDocumentInfo = $feedsApi->createFeedDocument($createFeedDocSpec);
    $feedDocumentId = $feedDocumentInfo->getFeedDocumentId();

    // Create XML document, where each <message> is a SKU and <quantity> is the stock level. Increment <messageID> for each SKU. Set <operationType> to 'Update'.
    $feedContents = "<?xml version='1.0' encoding='utf-8' ?>
    <AmazonEnvelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:noNamespaceSchemaLocation='amzn-envelope.xsd'>
    <Header>
    <DocumentVersion>1.01</DocumentVersion>
    <MerchantIdentifier>XXXXXXXXXX</MerchantIdentifier>
    </Header>
    <MessageType>Inventory</MessageType>";

    $i = 1;

    // Create loop to add each SKU to the XML document.
    foreach ($data as $key => $value) {

        // If $value['stock'] has a minus value, set to 0.
        if ( $value['stock'] < 0 ) {
            $value['stock'] = 0;
        }

        $feedContents .= '<Message>
        <MessageID>' . $i . '</MessageID>
        <OperationType>Update</OperationType>
        <Inventory>
        <SKU>' . $value['sku'] . '</SKU>
        <Quantity>' . $value['stock'] . '</Quantity>
        </Inventory>
        </Message>';

        $i++;
    }

    $feedContents .= '</AmazonEnvelope>';

    // The Document constructor accepts a custom \GuzzleHttp\Client object as an optional 3rd parameter. If that
    // parameter is passed, your custom Guzzle client will be used when uploading the feed document contents to Amazon.
    $docToUpload = new \SellingPartnerApi\Document($feedDocumentInfo, $feedType);
    $docToUpload->upload($feedContents);

    $createFeedSpec = new Feeds\CreateFeedSpecification();
    $createFeedSpec->setMarketplaceIds(['XXXXXXXXX']);
    $createFeedSpec->setInputFeedDocumentId($feedDocumentId);
    $createFeedSpec->setFeedType($feedType['name']);

    try {
        $feedsApi->createFeed($createFeedSpec);
    } catch (\SellingPartnerApi\ApiException $e) {
        // Capture the message in Sentry
        try {
            \Sentry\captureMessage( $e );
        } catch (\Exception $er) {
            \Sentry\captureMessage( $er );
        }
    }

}
@NoxArt
Copy link

NoxArt commented Dec 6, 2024

Hi, we're running what used to be product feeds in JSON right now, as for availability, I have that prepared, but not sure if I've tested it

I don't think JSON migration depends on version of this library? It's about sending a different feed imho, not sending it via different call

I think everything should stay the same except the // Create XML document section and setFeedType

FeedType is now always just JSON_LISTINGS_FEED

And what I'm generating right now looks like this
There are other approaches than PARTIAL_UPDATE, but I found it suitable when migrating from XML feeds ... like ... you can also have all information in 1 feed now, but if you already have generators and pipeline working with different types this is probably easier

{
	"header": {
		"sellerId": "xxxxx",
		"version": "2.0",
		"issueLocale": "en_US"
	},
	"messages": [
		{
		    "messageId": 1,
		    "sku": "xxxxx",
		    "operationType": "PARTIAL_UPDATE",
		    "productType": "BATTERY",
		    "attributes": {
		        "fulfillment_availability": [
		            {
		                "fulfillment_channel_code": "DEFAULT",
		                "quantity": 123,
		                "lead_time_to_ship_max_days": "5"
		            }
		        ]
		    }
		}
        ]
}

Keep in mind JSON has 10MB & 10k messages limit per feed

Hope this helps

@NoxArt
Copy link

NoxArt commented Dec 6, 2024

Now I'm not 100% sure the productType should be there ... just try it out and see

@jordan26
Copy link
Author

jordan26 commented Dec 6, 2024

Thanks @NoxArt - Responses from others were often just "read the documentation!", understandable but if you don't understand it, you're stuck. So I appreciate the extended reply with code.

I am taking this step by step, bare with me!

Changes:
#1 Update feed type
$feedType = FeedType::POST_INVENTORY_AVAILABILITY_DATA; to $feedType = 'FeedType::JSON_LISTINGS_FEED';

  • This is a perfect example of where I am immediately questioning myself such as, should it be $feedType = FeedType::JSON_LISTINGS_FEED or just $feedType = 'JSON_LISTINGS_FEED' etc. So easy to not get it right.

#2 Change XML to JSON data

$feedContents = json_encode([
            "header" => [
                "sellerId" => "XXXXXXXX",
                "version" => "2.0",
                "issueLocale" => "en_US"
            ],
            "messages" => [
                [
                    "messageId" => 1,
                    "sku" => "423953GE",
                    "operationType" => "PARTIAL_UPDATE",
                    "productType" => "BATTERY",
                    "attributes" => [
                        "fulfillment_availability" => [
                            [
                                "fulfillment_channel_code" => "DEFAULT",
                                "quantity" => 123,
                                "lead_time_to_ship_max_days" => "5"
                            ]
                        ]
                    ]
                ]
            ]
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

*Note, I am happy for this to be static during testing, I'll eventually make it dynamic with the SKUs and stock etc.

productType - Again, do I need to first do another call to get the appropriate value for here? For example, I am updating a pillow product, do I need to use a different productType first? So many questions I know, sorry.

... I am now even hitting an issue with the fact I used to use $feedType = FeedType:: which means I need to include the right namespace declaration at the top, such as:

use \SellingPartnerApi\Api\FeedsV20210630Api as FeedsApi;
use \SellingPartnerApi\FeedType;

Now I need to figure out the right version to use for that too. This is where I become so lost with so many little pieces to make sure is included, how I find which etc etc.

@NoxArt
Copy link

NoxArt commented Dec 6, 2024

$feedType = FeedType::JSON_LISTINGS_FEED or just $feedType = 'JSON_LISTINGS_FEED'

This isn't really related to Amazon API or this library, just general programming practices, both should yield the same result but the former is slightly better since it's generalized into a constant

Like I said I don't know for sure if you need productType to set availability, but I think you'll need it eventually
You should be able to list them through searchDefinitionsProductTypes (at least that was the method name in earlier versions of the API) ... and then somehow assign a correct productType to your products ... but this isn't really new ... in XML you also had product-type-specific elements

@jordan26
Copy link
Author

jordan26 commented Dec 10, 2024

At time of writing, I have managed to send my previous XML to Amazon successfully, whilst using the jlevers SP-API package (version 7.2.2).

Note: I am working on a JSON example too but have not yet figure the correct JSON payload/format to use, will update when I do

I have left in some conditionals unique to me like dev and staging do not send stock updates.

Notes:

  • MerchantIdentifier be sure to update.
  • This is being used on a WordPress install, therefore I have some functions such as update_option and delete_option in place to store the ID returned by Amazon for lookup on the next run.
/**
 * Use the jlevers/selling-partner-api library to send stock updates to Amazon. Build the XML file and send it as POST_INVENTORY_AVAILABILITY_DATA feed type.
 *
 * @param array $data
 */

    public function send_stock_to_amazon_xml( $data ) {

    // If environment is development or staging, don't send stock to Amazon.
    if( WP_ENV == 'development' || WP_ENV == 'staging' ) {
        return;
    }

    $connector = SellingPartnerApi::seller(
        clientId: defined('LWA_CLIENT_ID') ? LWA_CLIENT_ID : '',
        clientSecret: defined('LWA_CLIENT_SECRET') ? LWA_CLIENT_SECRET : '',
        refreshToken: defined('LWA_REFRESH_TOKEN') ? LWA_REFRESH_TOKEN : '',
        endpoint: Endpoint::EU,  // Or Endpoint::EU, Endpoint::FE, Endpoint::NA_SANDBOX, etc.
    );

    $feedType = "POST_INVENTORY_AVAILABILITY_DATA";
    $feedsApi = $connector->feedsV20210630();

    // Create feed document
    $contentType = CreateFeedDocumentResponse::getContentType($feedType);
    $createFeedDoc = new CreateFeedDocumentSpecification($contentType);
    $createDocumentResponse = $feedsApi->createFeedDocument($createFeedDoc);
    $feedDocument = $createDocumentResponse->dto();

    // Create XML document, where each <message> is a SKU and <quantity> is the stock level. Increment <messageID> for each SKU. Set <operationType> to 'Update'.
    $feedContents = "<?xml version='1.0' encoding='utf-8' ?>
    <AmazonEnvelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:noNamespaceSchemaLocation='amzn-envelope.xsd'>
    <Header>
    <DocumentVersion>1.01</DocumentVersion>
    <MerchantIdentifier>XXXXXXXXXX</MerchantIdentifier>
    </Header>
    <MessageType>Inventory</MessageType>";

    $i = 1;

    // Create loop to add each SKU to the XML document.
    foreach ($data as $key => $value) {

        // If $value['stock'] has a minus value, set to 0.
        if ( $value['stock'] < 0 ) {
            $value['stock'] = 0;
        }

        $feedContents .= '<Message>
        <MessageID>' . $i . '</MessageID>
        <OperationType>Update</OperationType>
        <Inventory>
        <SKU>' . $value['sku'] . '</SKU>
        <Quantity>' . $value['stock'] . '</Quantity>
        </Inventory>
        </Message>';

        $i++;
    }

    $feedContents .= '</AmazonEnvelope>';

    // Check if the previous feed ID is stored in the options table.
    $existing_feed_id = get_option('amazon_stock_feed_id');

    // If the feed ID is not stored in the options table, create a new feed.
    if ( !$existing_feed_id ) {

        // Upload feed contents to document
        $feedDocument->upload($feedType, $feedContents);

        $createFeedSpec = new CreateFeedSpecification(
            marketplaceIds: ['XXXXXXXXX'],
            inputFeedDocumentId: $feedDocument->feedDocumentId,
            feedType: $feedType,
        );

        // Create feed with the feed document we just uploaded
        $createFeedResponse = $feedsApi->createFeed($createFeedSpec);
        $feedId = $createFeedResponse->dto()->feedId;

        // Store the $feedId for later use.
        update_option('amazon_stock_feed_id', $feedId, false );

    } else {

        // Use the existing feed ID to check the status of the feed.
        $feed_status = $this->check_amazon_stock_feed_status($existing_feed_id);

        // If the feed status is 'DONE', delete the feed ID from the options table and create a new feed.
        if ( $feed_status === 'DONE' ) {

            // Delete the feed ID from the options table.
            delete_option('amazon_stock_feed_id');

            // Upload feed contents to document
            $feedDocument->upload($feedType, $feedContents);

            $createFeedSpec = new CreateFeedSpecification(
                marketplaceIds: ['XXXXXXXXX'],
                inputFeedDocumentId: $feedDocument->feedDocumentId,
                feedType: $feedType,
            );

            // Create feed with the feed document we just uploaded
            $createFeedResponse = $feedsApi->createFeed($createFeedSpec);
            $feedId = $createFeedResponse->dto()->feedId;

            // Store the $feedId for later use.
            update_option('amazon_stock_feed_id', $feedId, false );

        }

    }

}
private function check_amazon_stock_feed_status($feedId)
    {

        $connector = SellingPartnerApi::seller(
            clientId: defined('LWA_CLIENT_ID') ? LWA_CLIENT_ID : '',
            clientSecret: defined('LWA_CLIENT_SECRET') ? LWA_CLIENT_SECRET : '',
            refreshToken: defined('LWA_REFRESH_TOKEN') ? LWA_REFRESH_TOKEN : '',
            endpoint: Endpoint::EU,  // Or Endpoint::EU, Endpoint::FE, Endpoint::NA_SANDBOX, etc.
        );

        $feedsApi = $connector->feedsV20210630();

        try {
            $feed = $feedsApi->getFeed($feedId);

            if ($feed->dto()->processingStatus === 'DONE') {
                
                return 'DONE';

            } elseif ($feed->dto()->processingStatus !== 'DONE') {

                return $feed->dto()->processingStatus;
                
            }
        } catch (\Exception $e) {
            // Handle exception, send to Sentry etc.
            \Sentry\captureException($e);
        }
    }

@jlevers
Copy link
Owner

jlevers commented Dec 27, 2024

@jordan26, any luck? @NoxArt is correct that you don't need to upgrade to v7 in order to switch feed types, although I do recommend upgrading to v7 at some point as I'm no longer releasing updates to v5.

If you're only updating listing-related attributes (price, inventory, etc) it's safe to use the PRODUCT product type, rather than specifying the specific product type you're using. This simplifies the data requirements somewhat, as the JSON Schema for the PRODUCT product type is simpler than many of the other product type schemas.

Also, in case you're interested – I've created a converter tool that will convert the contents of your old POST_FLAT_FILE_INVLOADER_DATA into the JSON_LISTINGS_DATA format, so all you need to do is change the feed type in your code. You can get it here, and the relevant docs are here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants