diff --git a/docs/developer-portal/05-menu-integration-audit.md b/docs/developer-portal/05-menu-integration-audit.md new file mode 100644 index 0000000..771e712 --- /dev/null +++ b/docs/developer-portal/05-menu-integration-audit.md @@ -0,0 +1,25 @@ +# 05 Menu Integration Audit + +Source checked: Uber Eats "Menu Integration" section shared by you. + +## Implemented Now + +- Retrieve menu: + - `GET /api/v1/uber/menu` +- Full menu upload/replace: + - `PUT /api/v1/uber/menu/replace` (primary) +- Individual item updates: + - `POST /api/v1/uber/menu/items` + - supports stock/price style item-level updates via Menu Items endpoint + +## Existing Before + +- Legacy menu upsert helper route (`POST /api/v1/uber/menu/upsert`) +- Store/menu module structure and app-token auth for menu calls + +## Pending + +- Strict typed schemas for full menu payload entities (item, modifier group, category, menu) +- Validation rules for image metadata limits and alcoholic item classifications +- Dedicated mapper helpers for `core_price` and `bundled_items` enrichment + diff --git a/docs/developer-portal/05-menus.md b/docs/developer-portal/05-menus.md index 3ca4a72..4f3c4ab 100644 --- a/docs/developer-portal/05-menus.md +++ b/docs/developer-portal/05-menus.md @@ -4,9 +4,18 @@ Menu sync between POS and Uber Eats: - Full menu replacement via PUT - Fetch menu from Uber +- Item-level updates (out-of-stock/price updates) - Item and modifier mapping strategy - Validation and publish error handling Current wrapper route for full replacement: - `PUT /api/v1/uber/menu/replace` + +Item update route: + +- `POST /api/v1/uber/menu/items` + +Best-practice note: + +- Use API-managed menus only for integrated stores (avoid manual Menu Maker edits to prevent drift). diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index 81b551e..da4df80 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -230,7 +230,7 @@ }, "/api/v1/uber/menu/upsert": { "post": { - "summary": "Upsert store menu", + "summary": "Legacy upsert helper for store menu", "tags": [ "Uber Menu" ], @@ -254,6 +254,19 @@ } } }, + "/api/v1/uber/menu/items": { + "post": { + "summary": "Update individual menu items (stock/price updates)", + "tags": [ + "Uber Menu" + ], + "responses": { + "200": { + "description": "Menu items updated" + } + } + } + }, "/api/v1/uber/menu": { "get": { "summary": "Fetch store menu", diff --git a/postman/Uber_Wrapper.postman_collection.json b/postman/Uber_Wrapper.postman_collection.json index e23692a..a81d2dd 100644 --- a/postman/Uber_Wrapper.postman_collection.json +++ b/postman/Uber_Wrapper.postman_collection.json @@ -199,6 +199,39 @@ } } }, + { + "name": "Update Menu Items", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"items\": [\n {\n \"id\": \"item_1\",\n \"price_info\": {\n \"price\": 799\n },\n \"suspension_info\": {\n \"suspend_until\": null\n }\n }\n ]\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/v1/uber/menu/items", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "menu", + "items" + ] + } + } + }, { "name": "List Provisionable Stores", "request": { diff --git a/src/config/uberEndpoints.js b/src/config/uberEndpoints.js index 9a22e22..f7bd78d 100644 --- a/src/config/uberEndpoints.js +++ b/src/config/uberEndpoints.js @@ -1,7 +1,8 @@ module.exports = { menu: { upsert: "/v1/eats/stores/{storeId}/menus", - get: "/v1/eats/stores/{storeId}/menus" + get: "/v1/eats/stores/{storeId}/menus", + itemsUpdate: "/v1/eats/stores/{storeId}/menus/items" }, orders: { list: "/v1/eats/stores/{storeId}/orders", diff --git a/src/modules/proxy/proxy.controller.js b/src/modules/proxy/proxy.controller.js index 89d86e3..01265bf 100644 --- a/src/modules/proxy/proxy.controller.js +++ b/src/modules/proxy/proxy.controller.js @@ -57,6 +57,21 @@ async function replaceMenu(req, res) { return res.json({ success: true, data }); } +async function updateMenuItems(req, res) { + const schema = z.object({ + merchantId: z.string().min(1), + storeId: z.string().min(1), + items: z.array(z.any()).min(1) + }); + const payload = schema.parse(req.body); + const data = await proxyService.updateMenuItems({ + merchantId: payload.merchantId, + storeId: payload.storeId, + payload: { items: payload.items } + }); + return res.json({ success: true, data }); +} + async function listOrders(req, res) { const schema = z.object({ merchantId: z.string().min(1), @@ -208,6 +223,7 @@ module.exports = { genericProxy, upsertMenu, replaceMenu, + updateMenuItems, getMenu, listOrders, orderAction, diff --git a/src/modules/proxy/proxy.service.js b/src/modules/proxy/proxy.service.js index c3e9ccb..21f8138 100644 --- a/src/modules/proxy/proxy.service.js +++ b/src/modules/proxy/proxy.service.js @@ -151,6 +151,19 @@ async function menuGet({ merchantId, storeId }) { }); } +async function updateMenuItems({ merchantId, storeId, payload }) { + const uberPath = interpolatePath(uberEndpoints.menu.itemsUpdate, { storeId }); + return callUberApi({ + merchantId, + method: "POST", + uberPath, + body: payload, + wrapperRoute: "/api/v1/uber/menu/items", + authMode: "app", + scopes: AUTH_SCOPES.STORE + }); +} + async function ordersList({ merchantId, storeId, query }) { const uberPath = interpolatePath(uberEndpoints.orders.list, { storeId }); return callUberApi({ @@ -326,6 +339,7 @@ module.exports = { menuUpsert, menuReplace, menuGet, + updateMenuItems, ordersList, orderAction, updateStoreHours, diff --git a/src/routes/proxy.routes.js b/src/routes/proxy.routes.js index d980e41..a4d90a1 100644 --- a/src/routes/proxy.routes.js +++ b/src/routes/proxy.routes.js @@ -21,7 +21,7 @@ router.post("/uber/request", asyncHandler(controller.genericProxy)); * @openapi * /api/v1/uber/menu/upsert: * post: - * summary: Upsert store menu + * summary: Legacy upsert helper for store menu * tags: * - Uber Menu * responses: @@ -43,6 +43,19 @@ router.post("/uber/menu/upsert", asyncHandler(controller.upsertMenu)); */ router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu)); +/** + * @openapi + * /api/v1/uber/menu/items: + * post: + * summary: Update individual menu items (stock/price updates) + * tags: + * - Uber Menu + * responses: + * 200: + * description: Menu items updated + */ +router.post("/uber/menu/items", asyncHandler(controller.updateMenuItems)); + /** * @openapi * /api/v1/uber/menu: