diff --git a/.env.example b/.env.example index 4916a3b..e5d9af3 100644 --- a/.env.example +++ b/.env.example @@ -7,10 +7,12 @@ UBER_CLIENT_SECRET=your_client_secret UBER_REDIRECT_URI=http://localhost:8080/api/v1/auth/uber/callback UBER_OAUTH_BASE_URL=https://login.uber.com UBER_API_BASE_URL=https://api.uber.com +# Sandbox values when testing: +# UBER_OAUTH_BASE_URL=https://sandbox-auth.uber.com +# UBER_API_BASE_URL=https://test-api.uber.com # SQLite database path SQLITE_PATH=./data/uber_wrapper.db # Shared API key for wrapper clients (optional but recommended) WRAPPER_API_KEY=change-me - diff --git a/docs/developer-portal/01-getting-started-audit.md b/docs/developer-portal/01-getting-started-audit.md new file mode 100644 index 0000000..4e0cd2a --- /dev/null +++ b/docs/developer-portal/01-getting-started-audit.md @@ -0,0 +1,31 @@ +# 01 Getting Started Audit + +Source checked: Uber Eats "Getting Started" section shared by you. + +## Implemented Now + +- OAuth-based authentication foundation is present. +- Store/menu/order/webhook modules exist. +- Multi-merchant model is implemented. +- Sandbox guidance updated with official sandbox domains. +- Full menu replacement support added via `PUT /api/v1/uber/menu/replace`. + +## Partly Implemented + +- Store management: + Hours update exists, but typed online/offline and settings endpoints are pending. +- Order processing: + Core actions exist, but full BYOC and advanced workflows are pending. + +## Not Yet Implemented + +- Written-approval and scope entitlement checks (operational process outside API code). +- Formal quality gate automation for all production standards. +- Full typed coverage for promotions/reporting APIs. + +## Current Safe Fallback + +For any endpoint not yet typed, use: + +- `POST /api/v1/uber/request` + diff --git a/docs/developer-portal/05-menus.md b/docs/developer-portal/05-menus.md index cd81adf..3ca4a72 100644 --- a/docs/developer-portal/05-menus.md +++ b/docs/developer-portal/05-menus.md @@ -2,8 +2,11 @@ Menu sync between POS and Uber Eats: -- Upload/replace menu +- Full menu replacement via PUT - Fetch menu from Uber - Item and modifier mapping strategy - Validation and publish error handling +Current wrapper route for full replacement: + +- `PUT /api/v1/uber/menu/replace` diff --git a/docs/developer-portal/10-sandbox-testing.md b/docs/developer-portal/10-sandbox-testing.md index f0894c0..5c5358e 100644 --- a/docs/developer-portal/10-sandbox-testing.md +++ b/docs/developer-portal/10-sandbox-testing.md @@ -2,9 +2,20 @@ Checklist: +- Create a dedicated TESTING app in Uber Developer Dashboard +- Use sandbox domains in `.env`: + - `UBER_OAUTH_BASE_URL=https://sandbox-auth.uber.com` + - `UBER_API_BASE_URL=https://test-api.uber.com` - Test OAuth connect flow - Test menu upload/get - Test order lifecycle actions - Test webhook receipt and persistence - Validate retry and duplicate event handling +Validation sequence: + +1. Authentication token generation and authorized call test +2. Store status/hours API smoke tests +3. Full menu replacement test via PUT +4. Order webhook receipt and lifecycle action tests +5. Error and retry behavior checks diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index ed3a53c..6ec2fc9 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -202,6 +202,19 @@ } } }, + "/api/v1/uber/menu/replace": { + "put": { + "summary": "Replace store menu (full upload)", + "tags": [ + "Uber Menu" + ], + "responses": { + "200": { + "description": "Menu replaced" + } + } + } + }, "/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 4023ba8..a6dc0e5 100644 --- a/postman/Uber_Wrapper.postman_collection.json +++ b/postman/Uber_Wrapper.postman_collection.json @@ -83,6 +83,39 @@ } } }, + { + "name": "Replace Menu (PUT)", + "request": { + "method": "PUT", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"categories\": []\n }\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/v1/uber/menu/replace", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "menu", + "replace" + ] + } + } + }, { "name": "Generic Uber Request", "request": { @@ -135,4 +168,3 @@ } ] } - diff --git a/src/modules/proxy/proxy.controller.js b/src/modules/proxy/proxy.controller.js index 230d831..0e32be4 100644 --- a/src/modules/proxy/proxy.controller.js +++ b/src/modules/proxy/proxy.controller.js @@ -40,6 +40,21 @@ async function getMenu(req, res) { return res.json({ success: true, data }); } +async function replaceMenu(req, res) { + const schema = z.object({ + merchantId: z.string().min(1), + storeId: z.string().min(1), + menu: z.any() + }); + const payload = schema.parse(req.body); + const data = await proxyService.menuReplace({ + merchantId: payload.merchantId, + storeId: payload.storeId, + payload: payload.menu + }); + return res.json({ success: true, data }); +} + async function listOrders(req, res) { const schema = z.object({ merchantId: z.string().min(1), @@ -89,9 +104,9 @@ async function updateHours(req, res) { module.exports = { genericProxy, upsertMenu, + replaceMenu, getMenu, listOrders, orderAction, updateHours }; - diff --git a/src/modules/proxy/proxy.service.js b/src/modules/proxy/proxy.service.js index ea3d0b3..4080a94 100644 --- a/src/modules/proxy/proxy.service.js +++ b/src/modules/proxy/proxy.service.js @@ -94,6 +94,17 @@ async function menuUpsert({ merchantId, storeId, payload }) { }); } +async function menuReplace({ merchantId, storeId, payload }) { + const uberPath = interpolatePath(uberEndpoints.menu.upsert, { storeId }); + return callUberApi({ + merchantId, + method: "PUT", + uberPath, + body: payload, + wrapperRoute: "/api/v1/uber/menu/replace" + }); +} + async function menuGet({ merchantId, storeId }) { const uberPath = interpolatePath(uberEndpoints.menu.get, { storeId }); return callUberApi({ @@ -153,9 +164,9 @@ async function updateStoreHours({ merchantId, storeId, payload }) { module.exports = { genericProxy, menuUpsert, + menuReplace, menuGet, ordersList, orderAction, updateStoreHours }; - diff --git a/src/routes/proxy.routes.js b/src/routes/proxy.routes.js index c1e7a6c..7fca7a0 100644 --- a/src/routes/proxy.routes.js +++ b/src/routes/proxy.routes.js @@ -30,6 +30,19 @@ router.post("/uber/request", asyncHandler(controller.genericProxy)); */ router.post("/uber/menu/upsert", asyncHandler(controller.upsertMenu)); +/** + * @openapi + * /api/v1/uber/menu/replace: + * put: + * summary: Replace store menu (full upload) + * tags: + * - Uber Menu + * responses: + * 200: + * description: Menu replaced + */ +router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu)); + /** * @openapi * /api/v1/uber/menu: @@ -89,4 +102,3 @@ router.post("/uber/orders/:orderId/action", asyncHandler(controller.orderAction) router.put("/uber/stores/hours", asyncHandler(controller.updateHours)); module.exports = router; -