verbb / consume

A Craft CMS plugin to create HTTP requests in your Twig templates to consume JSON, XML or CSV content.
Other
4 stars 1 forks source link

Adding support for Fedex Trade Documents Upload API #9

Open bleepsandblops opened 6 months ago

bleepsandblops commented 6 months ago

What are you trying to do?

Hi there,

Thanks a lot for adding the regular Fedex API. Turns out they have another API called Trade Documents Upload API and it has a different URL: https://documentapi.prod.fedex.com/ and then endpoints: for documents: documents/v1/etds/upload for images: documents/v1/etds/upload

As far as I know, auth etc works like the other Fedex API.

Thank you!

What's your proposed solution?

Implement the Fedex Trade Upload API or add an option in the regular Fedex one

Additional context

No response

engram-design commented 6 months ago

You can also override the baseUri of the provider, which is probably going to be the easiest thing here, rather than spinning up a whole new provider, or adding a conditional for which API to use. Particularly if the auth is all the same.

{% set data = craft.consume.fetchData('fedex', 'POST', 'documents/v1/etds/upload', {
    base_uri: 'https://documentapi.prod.fedex.com/',
}) %}
bleepsandblops commented 6 months ago

Hi @engram-design thanks a lot, so would you say something like this should work? Because server is sending me back a 500 (see 500 error below)

        $payload = [
            'base_uri' => 'https://documentapi.prod.fedex.com/',
            'multipart' => [
                [
                    'name' => 'attachment',
                    'contents' => fopen(Craft::getAlias('@root') . $filePath, 'r'),
                    'filename' => $fileName
                ],
                [
                    'name' => 'document',
                    'contents' => json_encode([
                        'workflowName' => 'ETDPostshipment',
                        'name' => $fileName,
                        'contentType' => 'application/pdf',
                        'carrierCode' => 'FDXE',
                        'meta' => [
                            'trackingNumber' => $trackingNumber,
                            'shipmentDate' => $shipmentDate,
                            'shipDocumentType' => 'COMMERCIAL_INVOICE',
                            'originCountryCode' => 'FR',
                            'destinationCountryCode' => $order->shippingAddress->countryCode
                        ]
                    ])
                ]
            ]
        ];

$data = Consume::$plugin->getService()->fetchData('fedexTradeUpload', 'POST', 'documents/v1/etds/upload', $payload);

(I've also tried without the json_encode.)

The 500 error:

    "message": "Failed to parse multipart servlet request; nested exception is javax.servlet.ServletException: org.apache.tomcat.util.http.fileupload.FileUploadBase$InvalidContentTypeException: the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is text/xml; charset=utf-8",

For reference, this works:

        $headers = [
            'Authorization' => 'Bearer ' . $token, 
            'X-locale' => 'en_US',
            'Content-Type' => 'multipart/form-data'
        ];

        $payload =
            [
                'workflowName' => 'ETDPostshipment',
                'name' => 'XXX.pdf',
                'contentType' => 'application/pdf',
                'carrierCode' => 'FDXE',
                'meta' => [
                    'trackingNumber' => $trackingNumber,
                    'shipmentDate' => $shipmentDate,
                    'shipDocumentType' => 'COMMERCIAL_INVOICE',
                    'originCountryCode' => 'FR',
                    'destinationCountryCode' => $order->shippingAddress->countryCode
                ]
            ];

        $jsonPayload = json_encode($payload);

        $filePath = '/storage/XXX.pdf';
        $fileName = 'XXX.pdf'; // The name of the form field

        try {
            // Create a multipart/form-data stream
            $multipartStream = new MultipartStream([
                [
                    'name' => 'attachment',
                    'contents' => fopen(Craft::getAlias('@root') . $filePath, 'r'),
                    'filename' => basename($filePath)
                ],
                [
                    'name' => 'document',
                    'contents' => $jsonPayload
                ]
            ]);

            $headers = [
                'Authorization' => 'Bearer ' . $token, 
                'X-locale' => 'en_US',
                'Content-Type' => 'multipart/form-data; boundary=' . $multipartStream->getBoundary()
            ];

            // Create a PSR-7 request with the multipart stream as the body
            $request = new Request(
                'POST',
                'https://documentapi.prod.fedex.com/documents/v1/etds/upload', // Replace with your API endpoint
                $headers,
                $multipartStream
            );

            // Send the request
            $response = $client->send($request);

            echo $response->getBody();
        } catch (GuzzleException $e) {
            dump($e->getResponse()->getBody()->getContents());
        }
bleepsandblops commented 6 months ago

Ok so I had a dig in the code, and essentially this bit here is missing the 'multipart' right? https://github.com/verbb/auth/blob/d8ca65e982c0b0658dfd36a67abba3710015db23/src/base/ProviderTrait.php#L109

If I add in these lines this bit of code:

            if ($multipart = ArrayHelper::remove($options, 'multipart')) {
                $options['body'] =  $multipart;
                $boundary = ArrayHelper::remove($options, 'boundary');
                $options['headers']['Content-Type'] = 'multipart/form-data; boundary='.$boundary;
            }

and in my call:

$multipartStream = new MultipartStream([
            [
                'name' => 'attachment',
                'contents' => fopen(Craft::getAlias('@root') . $filePath, 'r'),
                'filename' => basename($filePath)
            ],
            [
                'name' => 'document',
                'contents' => json_encode([
                    'workflowName' => 'ETDPostshipment',
                    'name' => $fileName,
                    'contentType' => 'application/pdf',
                    'carrierCode' => 'FDXE',
                    'meta' => [
                        'trackingNumber' => $trackingNumber,
                        'shipmentDate' => $shipmentDate,
                        'shipDocumentType' => 'COMMERCIAL_INVOICE',
                        'originCountryCode' => 'FR',
                        'destinationCountryCode' => $order->shippingAddress->countryCode
                    ]
                ])
            ]
        ]);

        $payload = [
            'base_uri' => 'https://documentapi.prod.fedex.com/',
            'multipart' => $multipartStream,
            'boundary' => $multipartStream->getBoundary()
        ];

        $data = Consume::$plugin->getService()->fetchData('fedexTradeUpload', 'POST', 'documents/v1/etds/upload', $payload);

then it seems to work!

engram-design commented 6 months ago

Ah, good call. Yes we actually need to implement the shortcuts that Guzzle normally does, like json being auto-encoded as JSON, as technically it uses the league/oauth2-client package's getAuthenticatedRequest() method to do the authenticated request.

Just added that in https://github.com/verbb/auth/commit/d5e9c1e7477e28c765f54b1749cdfa7246ee8c54

bleepsandblops commented 6 months ago

@engram-design thanks! struggling with my composer would you mind sharing how I get that commit? I don't have auth as a standalone plugin, just through consume's requirements

engram-design commented 6 months ago

Yeah, you'll need to manually include the auth package via composer require verbb/auth:"dev-craft-4 as 1.0.15" which you can (and should) remove once I've tagged a release

bleepsandblops commented 6 months ago

This works great FYI - fine to close for me