diff --git a/docs/developer-portal/07-webhooks-audit.md b/docs/developer-portal/07-webhooks-audit.md index 4c67442..6d83649 100644 --- a/docs/developer-portal/07-webhooks-audit.md +++ b/docs/developer-portal/07-webhooks-audit.md @@ -16,6 +16,10 @@ Source checked: Uber Eats "Webhook" section shared by you. - `resource_id` - `resource_href` - signature and dedupe key +- Added explicit event handling for `store.menu_refresh_request`: + - mapped via `store_id` + - records latest menu refresh request metadata on `uber_connections` + - stores webhook UUID and `X-Environment` marker ## Existing Before @@ -27,4 +31,3 @@ Source checked: Uber Eats "Webhook" section shared by you. - Per-event downstream job workers (accept/deny SLA orchestration). - Alerting if order accept/deny not sent before timeout window. - Full typed schema validation per webhook `event_type`. - diff --git a/docs/developer-portal/07-webhooks.md b/docs/developer-portal/07-webhooks.md index 0652626..8afb6d5 100644 --- a/docs/developer-portal/07-webhooks.md +++ b/docs/developer-portal/07-webhooks.md @@ -25,8 +25,17 @@ Common event types handled: - `delivery.state_changed` - `store.provisioned` - `store.deprovisioned` +- `store.menu_refresh_request` - `store.status.changed` +Menu refresh handling: + +- On `store.menu_refresh_request`, wrapper records the request on the mapped Uber store connection. +- Persisted fields include: + - `last_menu_refresh_requested_at` + - `last_menu_refresh_webhook_uuid` + - `last_webhook_environment` (from `X-Environment`) + Retail fulfillment follow-up: - On `orders.fulfillment_issues.resolved`, fetch updated order details and inspect customer acknowledgment before next action. diff --git a/postman/Uber_Wrapper.postman_collection.json b/postman/Uber_Wrapper.postman_collection.json index 1882fe0..1fde0e9 100644 --- a/postman/Uber_Wrapper.postman_collection.json +++ b/postman/Uber_Wrapper.postman_collection.json @@ -1659,6 +1659,42 @@ } } }, + { + "name": "Webhook Ingest - Menu Refresh (Simulation)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Uber-Signature", + "value": "replace-with-valid-hmac" + }, + { + "key": "X-Environment", + "value": "production" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"event_type\": \"store.menu_refresh_request\",\n \"partner_store_id\": \"123456\",\n \"resource_href\": \"https://api.uber.com/v1/eats/stores/{{storeId}}\",\n \"store_id\": \"{{storeId}}\",\n \"webhook_meta\": {\n \"client_id\": \"app_client_id\",\n \"webhook_config_id\": \"merchant-integration.menu-refresh-request\",\n \"webhook_msg_timestamp\": 1622813397,\n \"webhook_msg_uuid\": \"b2340f4c-6dd7-4d65-a9bc-016631a2a13a\"\n }\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/v1/webhooks/uber", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "webhooks", + "uber" + ] + } + } + }, { "name": "Generic Uber Request", "request": { diff --git a/src/db/repositories.js b/src/db/repositories.js index c88cd0c..059fda2 100644 --- a/src/db/repositories.js +++ b/src/db/repositories.js @@ -107,6 +107,31 @@ const uberConnectionRepository = { return this.findByMerchantId(merchantId); }, + markMenuRefreshRequestedByStoreId(uberStoreId, payload) { + const existing = this.findByUberStoreId(uberStoreId); + if (!existing) { + return null; + } + const timestamp = nowIso(); + db.prepare( + ` + UPDATE uber_connections + SET last_menu_refresh_requested_at = ?, + last_menu_refresh_webhook_uuid = ?, + last_webhook_environment = ?, + updated_at = ? + WHERE uber_store_id = ? + ` + ).run( + payload?.requestedAt || timestamp, + payload?.webhookMsgUuid || null, + payload?.environment || null, + timestamp, + uberStoreId + ); + return this.findByUberStoreId(uberStoreId); + }, + list() { return db .prepare(` diff --git a/src/db/sqlite.js b/src/db/sqlite.js index 20d304a..bf44c5f 100644 --- a/src/db/sqlite.js +++ b/src/db/sqlite.js @@ -120,6 +120,16 @@ function initSchema() { if (!tableHasColumn("api_logs", "order_id")) { db.exec("ALTER TABLE api_logs ADD COLUMN order_id TEXT"); } + + if (!tableHasColumn("uber_connections", "last_menu_refresh_requested_at")) { + db.exec("ALTER TABLE uber_connections ADD COLUMN last_menu_refresh_requested_at TEXT"); + } + if (!tableHasColumn("uber_connections", "last_menu_refresh_webhook_uuid")) { + db.exec("ALTER TABLE uber_connections ADD COLUMN last_menu_refresh_webhook_uuid TEXT"); + } + if (!tableHasColumn("uber_connections", "last_webhook_environment")) { + db.exec("ALTER TABLE uber_connections ADD COLUMN last_webhook_environment TEXT"); + } } module.exports = { diff --git a/src/modules/webhooks/webhooks.controller.js b/src/modules/webhooks/webhooks.controller.js index ed7c10a..799056d 100644 --- a/src/modules/webhooks/webhooks.controller.js +++ b/src/modules/webhooks/webhooks.controller.js @@ -87,6 +87,23 @@ function applyProvisioningStateFromWebhook(eventType, payload) { uberConnectionRepository.setStatusByMerchantId(connection.merchant_id, nextStatus); } +function applyMenuRefreshRequestFromWebhook(eventType, payload, headers) { + if (eventType !== "store.menu_refresh_request") { + return; + } + + const storeId = extractStoreId(payload); + if (!storeId) { + return; + } + + uberConnectionRepository.markMenuRefreshRequestedByStoreId(String(storeId), { + requestedAt: new Date().toISOString(), + webhookMsgUuid: payload?.webhook_meta?.webhook_msg_uuid || null, + environment: headers?.["x-environment"] || null + }); +} + async function handleUberWebhook(req, res) { if (!verifyBasicAuthIfConfigured(req)) { return res.status(401).json({ @@ -133,6 +150,7 @@ async function handleUberWebhook(req, res) { }); applyProvisioningStateFromWebhook(eventType, req.body || {}); + applyMenuRefreshRequestFromWebhook(eventType, req.body || {}, req.headers || {}); return res.status(200).end(); }