Align Upload Menu to v2 with gzip request encoding

This commit is contained in:
MOHAN 2026-03-29 18:37:24 +05:30
parent 5ea2d86a48
commit d80f9e9baf
8 changed files with 47 additions and 10 deletions

View File

@ -11,6 +11,10 @@ Source checked: Uber Eats "Menu Integration" section shared by you.
- applies `Accept-Encoding: gzip` for large payload responses - applies `Accept-Encoding: gzip` for large payload responses
- Full menu upload/replace: - Full menu upload/replace:
- `PUT /api/v1/uber/menu/replace` (primary) - `PUT /api/v1/uber/menu/replace` (primary)
- aligned to upstream `PUT /v2/eats/stores/{store_id}/menus`
- wrapper uploads gzip-compressed JSON (`Content-Encoding: gzip`)
- supports optional `menu.menu_type` (delivery/pick-up/dine-in)
- validates known menu_type enum values
- Individual item updates: - Individual item updates:
- `POST /api/v1/uber/menu/items` - `POST /api/v1/uber/menu/items`
- supports stock/price style item-level updates via Menu Items endpoint - supports stock/price style item-level updates via Menu Items endpoint

View File

@ -11,6 +11,12 @@ Menu sync between POS and Uber Eats:
Current wrapper route for full replacement: Current wrapper route for full replacement:
- `PUT /api/v1/uber/menu/replace` - `PUT /api/v1/uber/menu/replace`
- upstream mapped to `PUT /v2/eats/stores/{store_id}/menus`
- request body is gzip-compressed for upload (`Content-Encoding: gzip`, `Content-Type: application/json`)
- optional `menu.menu_type` supported:
- `MENU_TYPE_FULFILLMENT_DELIVERY` (default)
- `MENU_TYPE_FULFILLMENT_PICK_UP`
- `MENU_TYPE_FULFILLMENT_DINE_IN`
Item update route: Item update route:
@ -29,3 +35,4 @@ Menu fetch route:
Best-practice note: Best-practice note:
- Use API-managed menus only for integrated stores (avoid manual Menu Maker edits to prevent drift). - Use API-managed menus only for integrated stores (avoid manual Menu Maker edits to prevent drift).
- Alcoholic item flag is effectively sticky in Uber (`alcoholic_items > 0` cannot be reverted by API update).

View File

@ -305,13 +305,13 @@
}, },
"/api/v1/uber/menu/replace": { "/api/v1/uber/menu/replace": {
"put": { "put": {
"summary": "Replace store menu (full upload)", "summary": "Upload/replace store menu (PUT /v2/eats/stores/{store_id}/menus)",
"tags": [ "tags": [
"Uber Menu" "Uber Menu"
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Menu replaced" "description": "Menu replaced (Uber returns 204 No Content)"
} }
} }
} }

View File

@ -167,7 +167,7 @@
} }
}, },
{ {
"name": "Replace Menu (PUT)", "name": "Upload Menu (PUT v2)",
"request": { "request": {
"method": "PUT", "method": "PUT",
"header": [ "header": [
@ -182,7 +182,7 @@
], ],
"body": { "body": {
"mode": "raw", "mode": "raw",
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"categories\": []\n }\n}" "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"menu_type\": \"MENU_TYPE_FULFILLMENT_DELIVERY\",\n \"menus\": [],\n \"categories\": [],\n \"items\": [],\n \"modifier_groups\": []\n }\n}"
}, },
"url": { "url": {
"raw": "{{baseUrl}}/api/v1/uber/menu/replace", "raw": "{{baseUrl}}/api/v1/uber/menu/replace",

View File

@ -1,6 +1,7 @@
module.exports = { module.exports = {
menu: { menu: {
upsert: "/v1/eats/stores/{storeId}/menus", upsert: "/v1/eats/stores/{storeId}/menus",
upload: "/v2/eats/stores/{storeId}/menus",
get: "/v2/eats/stores/{storeId}/menus", get: "/v2/eats/stores/{storeId}/menus",
itemsUpdate: "/v1/eats/stores/{storeId}/menus/items" itemsUpdate: "/v1/eats/stores/{storeId}/menus/items"
}, },

View File

@ -67,6 +67,20 @@ async function replaceMenu(req, res) {
menu: z.any() menu: z.any()
}); });
const payload = schema.parse(req.body); const payload = schema.parse(req.body);
const allowedMenuTypes = new Set([
"MENU_TYPE_FULFILLMENT_DELIVERY",
"MENU_TYPE_FULFILLMENT_PICK_UP",
"MENU_TYPE_FULFILLMENT_DINE_IN"
]);
const menuType = payload.menu && payload.menu.menu_type;
if (menuType && !allowedMenuTypes.has(menuType)) {
const error = new Error(
"menu.menu_type must be one of MENU_TYPE_FULFILLMENT_DELIVERY, MENU_TYPE_FULFILLMENT_PICK_UP, MENU_TYPE_FULFILLMENT_DINE_IN"
);
error.status = 400;
throw error;
}
const data = await proxyService.menuReplace({ const data = await proxyService.menuReplace({
merchantId: payload.merchantId, merchantId: payload.merchantId,
storeId: payload.storeId, storeId: payload.storeId,

View File

@ -1,4 +1,6 @@
const axios = require("axios"); const axios = require("axios");
const zlib = require("zlib");
const { promisify } = require("util");
const env = require("../../config/env"); const env = require("../../config/env");
const uberEndpoints = require("../../config/uberEndpoints"); const uberEndpoints = require("../../config/uberEndpoints");
const { uberConnectionRepository, apiLogRepository } = require("../../db/adapter"); const { uberConnectionRepository, apiLogRepository } = require("../../db/adapter");
@ -11,6 +13,8 @@ const uberApiClient = axios.create({
timeout: 30000 timeout: 30000
}); });
const gzipAsync = promisify(zlib.gzip);
function interpolatePath(pathTemplate, params = {}) { function interpolatePath(pathTemplate, params = {}) {
let output = pathTemplate; let output = pathTemplate;
Object.entries(params).forEach(([key, value]) => { Object.entries(params).forEach(([key, value]) => {
@ -61,6 +65,7 @@ async function callUberApi({
uberPath, uberPath,
query, query,
body, body,
logRequestBody,
wrapperRoute, wrapperRoute,
authMode, authMode,
scopes, scopes,
@ -93,7 +98,7 @@ async function callUberApi({
wrapperRoute, wrapperRoute,
uberPath, uberPath,
responseStatus: response.status, responseStatus: response.status,
requestBody: body, requestBody: logRequestBody !== undefined ? logRequestBody : body,
responseBody: response.data responseBody: response.data
}); });
@ -107,7 +112,7 @@ async function callUberApi({
wrapperRoute, wrapperRoute,
uberPath, uberPath,
responseStatus: normalized.status, responseStatus: normalized.status,
requestBody: body, requestBody: logRequestBody !== undefined ? logRequestBody : body,
responseBody: { responseBody: {
code: normalized.code, code: normalized.code,
message: normalized.message, message: normalized.message,
@ -147,12 +152,18 @@ async function menuUpsert({ merchantId, storeId, payload }) {
} }
async function menuReplace({ merchantId, storeId, payload }) { async function menuReplace({ merchantId, storeId, payload }) {
const uberPath = interpolatePath(uberEndpoints.menu.upsert, { storeId }); const uberPath = interpolatePath(uberEndpoints.menu.upload, { storeId });
const compressedPayload = await gzipAsync(Buffer.from(JSON.stringify(payload), "utf8"));
return callUberApi({ return callUberApi({
merchantId, merchantId,
method: "PUT", method: "PUT",
uberPath, uberPath,
body: payload, body: compressedPayload,
logRequestBody: payload,
headers: {
"Content-Encoding": "gzip",
"Content-Type": "application/json"
},
wrapperRoute: "/api/v1/uber/menu/replace", wrapperRoute: "/api/v1/uber/menu/replace",
authMode: "app", authMode: "app",
scopes: AUTH_SCOPES.STORE scopes: AUTH_SCOPES.STORE

View File

@ -34,12 +34,12 @@ router.post("/uber/menu/upsert", asyncHandler(controller.upsertMenu));
* @openapi * @openapi
* /api/v1/uber/menu/replace: * /api/v1/uber/menu/replace:
* put: * put:
* summary: Replace store menu (full upload) * summary: Upload/replace store menu (PUT /v2/eats/stores/{store_id}/menus)
* tags: * tags:
* - Uber Menu * - Uber Menu
* responses: * responses:
* 200: * 200:
* description: Menu replaced * description: Menu replaced (Uber returns 204 No Content)
*/ */
router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu)); router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu));