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
- 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

View File

@ -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).

View File

@ -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)"
}
}
}

View File

@ -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",

View File

@ -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"
},

View File

@ -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,

View File

@ -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

View File

@ -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));