amazon-php / sp-api-sdk

Amazon Selling Partner SPI - PHP SDKs
130 stars 53 forks source link

Sandbox Mode Integration #174

Closed SunnyFlail closed 2 years ago

SunnyFlail commented 2 years ago

Hi, we have a problem with integrating the sandbox API. We're using Guzzle as Client, using middlewares I managed to call the sandbox endpoints manually, but got The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been... as an response, so I figured out there was some kind of problem with singing the request. I think it is a pretty much necessary feature, so I wondered whether you will implement it in your sdk , or is there some kind of battle-tested workaround of this problem.

Thanks, Patrick

sw-Oldeu commented 2 years ago

I searched for it too and I deceided to patch the AmazonPHP\SellingPartner\Configuration class. I added am member sandbox and changed the method apiURL. I just prefixed the host with sandbox:

public function apiURL(string $awsRegion) : string
    {
        if (!Regions::isValid($awsRegion)) {
            throw new InvalidArgumentException("Invalid region {$awsRegion}");
        }
        $apiUrl = null;
        switch ($awsRegion) {
            case Regions::EUROPE:
                $apiUrl = Regions::EUROPE_URL;
                break;
            case Regions::FAR_EAST:
                $apiUrl = Regions::FAR_EAST_URL;
                break;
            case Regions::NORTH_AMERICA:
                $apiUrl = Regions::NORTH_AMERICA_URL;
                break;
            default:
                throw new \RuntimeException('unknown region');
        }
        if ($this->useSandBox) {
            $apiUrl = substr_replace($apiUrl, 'sandbox.', 8, 0);
        }
        return $apiUrl;
    }

Unfortunately there is no way to modify the request object not even with plugins yet, because they are passed to the plugin method, but the reqeust object is immutable as the specification PSR-18 requires it.

When I'm bulding the Configuration I do it this way:

        $configuration = Configuration::forIAMUser(
            'clientId',
            'clientSecret',
            'accessKey',
            'secretKey'
        );
        $configuration->setSandbox(true);

This is the smallest workaround for me and doesn't affect production at all. It's maybe not the right way, but it works for me.

Regards

norberttech commented 2 years ago

hey, I'm not entirely familiar with the sandbox, so @sw-Oldeu you are saying that it should be enough just to adjust the URL in order to make sandox available? If that's the case I don't see a reason for not making it part of the config just like you did

Unfortunately there is no way to modify the request object not even with plugins yet, because they are passed to the > plugin method, but the reqeust object is immutable as the specification PSR-18 requires it.

Could you please elaborate on this one? What changes are needed on the request object? Or maybe you are saying that modification of the request object would be an alternative approach to the one you showed above?

zviryatko commented 2 years ago

@norberttech here the doc — https://developer-docs.amazon.com/sp-api/docs/the-selling-partner-api-sandbox

sw-Oldeu commented 2 years ago

First I tried to add middleware to the Guzzle HTTP Client and prefix the URL like @SunnyFlail did. I tried it with the example retrieving a Catalog Item (this call: $item = $sdk->catalogItem()->getCatalogItem()) and this wasn't working. It's because in the method AmazonPHP\SellingPartner\Api\CatalogApi\CatalogItemSDK::getCatalogItemRequest a request Object is built and used for the header signature (in the return statement). And after the request is built preRequest extensions are applied here.

Yes, changing the request object could be an alternative approach but I think an ugly one. Even if you could modify the request, you would have to re-calculate the header signature. But this is not possible because calling the methods withHeader or withUri will return a new instance of this request object and won't modify this object (It's required from PSR Interfaces). To be able to modify the request Object (don't know if this would be a good idea) this call

$this->configuration->extensions()->preRequest('CatalogItem', 'getCatalogItem', $request);

should be modified to

$request = $this->configuration->extensions()->preRequest('CatalogItem', 'getCatalogItem', $request);

Another option could be applying a plugin in AmazonPHP\SellingPartner\HttpSignatureHeaders to modify the request before calculate the signature. But telling the Configuration to use sandbox mode was more obvious to me. Because this was just one method to change.

To be honest: I changed this and tried to make successful calls to the API. I logged the called URLs and that was enough for me to approve this works because I didn't get any errors. Because my change won't hurt my production environment even if my modification will be removed with an update and it's not having any BC I think this is a good idea.

sw-Oldeu commented 2 years ago

@norberttech what do you think about that? I'm not very experienced with this, but I could post the changes here or I could make a PR for this. Or would you say this would lead to some code smells?

norberttech commented 2 years ago

hey @sw-Oldeu if you say that simply adding sandbox to each url solves the problem I would say lets adjust the config and make it possible. Changing requests on the fly through extensions sounds to me a bit over-complicated

sw-Oldeu commented 2 years ago

As I said, I have not really tested it out. Maybe I can make some calls tomorrow and compare with the result in the docs.

Oh and I see now, that not all Regions have sandboxes available as it is described here. I don't know which way should be preferred: Throwing an exception using sandbos mode for unsupported regions or just pass through the expectable 404 from amazon?

norberttech commented 2 years ago

There are only 3 regions, North America, Europe and Far East, all seem to be supported, what I am missing?

image
sw-Oldeu commented 2 years ago

@norberttech sorry for not replying. I tested it out but it seems there are limitations. I was not able to create a restricted data token (RDT) in sandbox environment. But I can create one in production environment. If I use my RDT to fetch an order in sandbox it works. Maybe the sandbox cannot create an RDT? I think since the sandbox just gives you the prepared data I could determine if I'm using sandbox or not. And when in sandbox, I must not use the RDT but a normal access token. With this my sandbox testcalls are working.

Maybe I'm doing it wrong? The sdk is throwing an AmazonPHP\SellingPartner\Exception\ApiException with message [400] Error connecting to the API (https://sandbox.sellingpartnerapi-eu.amazon.com/tokens/2021-03-01/restrictedDataToken) with the error response:

[
  {
    "code": "InvalidInput",
    "message": "Could not match input arguments"
  }
]

The Body I send is

{
  "restrictedResources": [
    {
      "method": "GET",
      "path": "/orders/v0/orders",
      "dataElements": [
        "buyerInfo",
        "shippingAddress"
      ]
    }
  ]
}

Now my Problem is, that I currently do not have any more time for at least 4 Weeks to dig deeper into this. Maybe others can. All I did was prepending sandbox to the apiURL and the apiHost as well.

Regards

owsiakl commented 2 years ago

Hey @sw-Oldeu, to successfully call for RDT you need to provide exactly same data like defined in the schema.

There are some exceptions to this - looks like it depends on the API, but in the case of sandbox tokens API:

Everything else will result in Could not match input arguments exception. I've tested this request:

{
  "restrictedResources": [
    {
      "method": "GET",
      "path": "/orders/v0/orders/943-12-123434/address",
      "dataElements": [
        "buyerInfo",
        "shippingAddress"
      ]
    }
  ]
}

And it works fine(although looks like returned token can't be used in sandboxed orders API - it have to be a regular access token).

I've also checked sandboxed orders & inbound APIs. As long as we stick with request parameters that Amazon expects, we're good - what needs to be send is defined in each API's schema in x-amzn-api-sandbox object. Static sandbox is veeery limited here.

When it comes to dynamic sandbox - it would be awesome to have it, but looks like only couple of vendor APIs endpoints supports this. The rest is using static sandbox, but I think there are no difference between them - both are using same URLs and same SDKs, so no additional code is needed here.


I've testes those requests: Request uri for fetching orders:

https://sandbox.sellingpartnerapi-na.amazon.com/orders/v0/orders?CreatedAfter=TEST_CASE_200&MarketplaceIds=ATVPDKIKX0DER

Request parameters for creating inbound shipment:

{
  "InboundShipmentHeader": {
    "ShipmentName": "43545345",
    "ShipFromAddress": {
      "Name": "35435345",
      "AddressLine1": "123 any st",
      "DistrictOrCounty": "Washtenaw",
      "City": "Ann Arbor",
      "StateOrProvinceCode": "Test",
      "CountryCode": "US",
      "PostalCode": "48103"
    },
    "DestinationFulfillmentCenterId": "AEB2",
    "AreCasesRequired": true,
    "ShipmentStatus": "WORKING",
    "LabelPrepPreference": "SELLER_LABEL",
    "IntendedBoxContentsSource": "NONE"
  },
  "InboundShipmentItems": [
    {
      "ShipmentId": "345453",
      "SellerSKU": "34534545",
      "FulfillmentNetworkSKU": "435435435",
      "QuantityShipped": 0,
      "QuantityReceived": 0,
      "QuantityInCase": 0,
      "ReleaseDate": "2020-04-23",
      "PrepDetailsList": [
        {
          "PrepInstruction": "Polybagging",
          "PrepOwner": "AMAZON"
        }
      ]
    }
  ],
  "MarketplaceId": "MarketplaceId"
}
norberttech commented 2 years ago

Implemented by #212 and #213