From d80f9e9baf39536e304dfb1d39bdef39f9f97220 Mon Sep 17 00:00:00 2001 From: MOHAN Date: Sun, 29 Mar 2026 18:37:24 +0530 Subject: [PATCH] Align Upload Menu to v2 with gzip request encoding --- .../05-menu-integration-audit.md | 4 ++++ docs/developer-portal/05-menus.md | 7 +++++++ docs/openapi/openapi.json | 4 ++-- postman/Uber_Wrapper.postman_collection.json | 4 ++-- src/config/uberEndpoints.js | 1 + src/modules/proxy/proxy.controller.js | 14 ++++++++++++++ src/modules/proxy/proxy.service.js | 19 +++++++++++++++---- src/routes/proxy.routes.js | 4 ++-- 8 files changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/developer-portal/05-menu-integration-audit.md b/docs/developer-portal/05-menu-integration-audit.md index 78203fe..c51489e 100644 --- a/docs/developer-portal/05-menu-integration-audit.md +++ b/docs/developer-portal/05-menu-integration-audit.md @@ -11,6 +11,10 @@ Source checked: Uber Eats "Menu Integration" section shared by you. - applies `Accept-Encoding: gzip` for large payload responses - Full menu upload/replace: - `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: - `POST /api/v1/uber/menu/items` - supports stock/price style item-level updates via Menu Items endpoint diff --git a/docs/developer-portal/05-menus.md b/docs/developer-portal/05-menus.md index 5f2e38b..d23d041 100644 --- a/docs/developer-portal/05-menus.md +++ b/docs/developer-portal/05-menus.md @@ -11,6 +11,12 @@ Menu sync between POS and Uber Eats: Current wrapper route for full replacement: - `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: @@ -29,3 +35,4 @@ Menu fetch route: Best-practice note: - 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). diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index 7c685cf..e4576fc 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -305,13 +305,13 @@ }, "/api/v1/uber/menu/replace": { "put": { - "summary": "Replace store menu (full upload)", + "summary": "Upload/replace store menu (PUT /v2/eats/stores/{store_id}/menus)", "tags": [ "Uber Menu" ], "responses": { "200": { - "description": "Menu replaced" + "description": "Menu replaced (Uber returns 204 No Content)" } } } diff --git a/postman/Uber_Wrapper.postman_collection.json b/postman/Uber_Wrapper.postman_collection.json index 7bd873b..d4352ca 100644 --- a/postman/Uber_Wrapper.postman_collection.json +++ b/postman/Uber_Wrapper.postman_collection.json @@ -167,7 +167,7 @@ } }, { - "name": "Replace Menu (PUT)", + "name": "Upload Menu (PUT v2)", "request": { "method": "PUT", "header": [ @@ -182,7 +182,7 @@ ], "body": { "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": { "raw": "{{baseUrl}}/api/v1/uber/menu/replace", diff --git a/src/config/uberEndpoints.js b/src/config/uberEndpoints.js index f704827..648516d 100644 --- a/src/config/uberEndpoints.js +++ b/src/config/uberEndpoints.js @@ -1,6 +1,7 @@ module.exports = { menu: { upsert: "/v1/eats/stores/{storeId}/menus", + upload: "/v2/eats/stores/{storeId}/menus", get: "/v2/eats/stores/{storeId}/menus", itemsUpdate: "/v1/eats/stores/{storeId}/menus/items" }, diff --git a/src/modules/proxy/proxy.controller.js b/src/modules/proxy/proxy.controller.js index c612403..603aa72 100644 --- a/src/modules/proxy/proxy.controller.js +++ b/src/modules/proxy/proxy.controller.js @@ -67,6 +67,20 @@ async function replaceMenu(req, res) { menu: z.any() }); 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({ merchantId: payload.merchantId, storeId: payload.storeId, diff --git a/src/modules/proxy/proxy.service.js b/src/modules/proxy/proxy.service.js index 2a881f5..193d5f3 100644 --- a/src/modules/proxy/proxy.service.js +++ b/src/modules/proxy/proxy.service.js @@ -1,4 +1,6 @@ const axios = require("axios"); +const zlib = require("zlib"); +const { promisify } = require("util"); const env = require("../../config/env"); const uberEndpoints = require("../../config/uberEndpoints"); const { uberConnectionRepository, apiLogRepository } = require("../../db/adapter"); @@ -11,6 +13,8 @@ const uberApiClient = axios.create({ timeout: 30000 }); +const gzipAsync = promisify(zlib.gzip); + function interpolatePath(pathTemplate, params = {}) { let output = pathTemplate; Object.entries(params).forEach(([key, value]) => { @@ -61,6 +65,7 @@ async function callUberApi({ uberPath, query, body, + logRequestBody, wrapperRoute, authMode, scopes, @@ -93,7 +98,7 @@ async function callUberApi({ wrapperRoute, uberPath, responseStatus: response.status, - requestBody: body, + requestBody: logRequestBody !== undefined ? logRequestBody : body, responseBody: response.data }); @@ -107,7 +112,7 @@ async function callUberApi({ wrapperRoute, uberPath, responseStatus: normalized.status, - requestBody: body, + requestBody: logRequestBody !== undefined ? logRequestBody : body, responseBody: { code: normalized.code, message: normalized.message, @@ -147,12 +152,18 @@ async function menuUpsert({ 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({ merchantId, method: "PUT", uberPath, - body: payload, + body: compressedPayload, + logRequestBody: payload, + headers: { + "Content-Encoding": "gzip", + "Content-Type": "application/json" + }, wrapperRoute: "/api/v1/uber/menu/replace", authMode: "app", scopes: AUTH_SCOPES.STORE diff --git a/src/routes/proxy.routes.js b/src/routes/proxy.routes.js index 174ccd2..4df7082 100644 --- a/src/routes/proxy.routes.js +++ b/src/routes/proxy.routes.js @@ -34,12 +34,12 @@ router.post("/uber/menu/upsert", asyncHandler(controller.upsertMenu)); * @openapi * /api/v1/uber/menu/replace: * put: - * summary: Replace store menu (full upload) + * summary: Upload/replace store menu (PUT /v2/eats/stores/{store_id}/menus) * tags: * - Uber Menu * responses: * 200: - * description: Menu replaced + * description: Menu replaced (Uber returns 204 No Content) */ router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu));