diff --git a/docs/developer-portal/07-webhooks-audit.md b/docs/developer-portal/07-webhooks-audit.md index 6d83649..0c217ab 100644 --- a/docs/developer-portal/07-webhooks-audit.md +++ b/docs/developer-portal/07-webhooks-audit.md @@ -20,6 +20,10 @@ Source checked: Uber Eats "Webhook" section shared by you. - mapped via `store_id` - records latest menu refresh request metadata on `uber_connections` - stores webhook UUID and `X-Environment` marker +- Added explicit event handling for `eats.report.success`: + - maps by `job_id` / `workflow_id` + - marks report jobs completed + - persists report sections metadata for download orchestration ## Existing Before diff --git a/docs/developer-portal/07-webhooks.md b/docs/developer-portal/07-webhooks.md index 8afb6d5..724cc78 100644 --- a/docs/developer-portal/07-webhooks.md +++ b/docs/developer-portal/07-webhooks.md @@ -27,6 +27,7 @@ Common event types handled: - `store.deprovisioned` - `store.menu_refresh_request` - `store.status.changed` +- `eats.report.success` Menu refresh handling: diff --git a/docs/developer-portal/16-reporting-audit.md b/docs/developer-portal/16-reporting-audit.md index 683186e..1d20990 100644 --- a/docs/developer-portal/16-reporting-audit.md +++ b/docs/developer-portal/16-reporting-audit.md @@ -6,6 +6,7 @@ Source checked: Uber Eats "Reporting Guide" section shared by you. - Dedicated Reporting route: - `POST /api/v1/uber/reporting/fetch` + - `POST /api/v1/uber/reporting/create` - Uses `eats.report` client-credentials scope. - Retry policy implemented for safe transient failures only: - `429`, `408`, `500`, `502`, `503`, `504`, and network errors @@ -16,6 +17,11 @@ Source checked: Uber Eats "Reporting Guide" section shared by you. - ignore unknown extra columns - tolerate missing columns with null/default behavior - report missing required headers in response metadata +- Marketplace Reporting API async flow aligned: + - `POST /v1/eats/report` wrapper support with report-type/date constraints + - workflow tracking via `workflow_id` + - webhook completion handling for `eats.report.success` + - report sections metadata persisted on completion ## Existing Before @@ -24,7 +30,5 @@ Source checked: Uber Eats "Reporting Guide" section shared by you. ## Pending -- Final typed endpoint wrappers for specific reporting reference endpoints once exact paths are shared -- Overnight polling scheduler/job orchestration +- Overnight polling scheduler/job orchestration for report section downloads - Reconciliation materialization tables for settled vs provisional values - diff --git a/docs/developer-portal/16-reporting.md b/docs/developer-portal/16-reporting.md index 4cf8dac..b3b5b20 100644 --- a/docs/developer-portal/16-reporting.md +++ b/docs/developer-portal/16-reporting.md @@ -9,6 +9,7 @@ Reporting focus: Typed route: - `POST /api/v1/uber/reporting/fetch` +- `POST /api/v1/uber/reporting/create` Key behavior: @@ -16,4 +17,6 @@ Key behavior: - Retries transient errors only (`408`, `429`, `500`, `502`, `503`, `504`, network timeouts) - Parses CSV by header names (not fixed column positions) - Tolerates unknown columns and missing optional fields - +- Reporting job flow: + - create job returns `workflow_id` + - completion arrives via webhook `eats.report.success` diff --git a/docs/developer-portal/20-marketplace-reporting-api-audit.md b/docs/developer-portal/20-marketplace-reporting-api-audit.md new file mode 100644 index 0000000..38e5a65 --- /dev/null +++ b/docs/developer-portal/20-marketplace-reporting-api-audit.md @@ -0,0 +1,28 @@ +# 20 Marketplace Reporting API 1.0.0 Audit + +Source checked: Uber Eats "Marketplace Reporting API (1.0.0)" section shared by you. + +## Implemented Now + +- Added official create-report wrapper route: + - `POST /api/v1/uber/reporting/create` + - upstream `POST /v1/eats/report` + - returns `workflow_id` +- Added request validation: + - `report_type` enum validation (10 report types) + - requires at least one `store_uuids` or `group_uuids` + - validates date format by report type + - validates `start_date <= end_date` + - validates range/lookback constraints from the docs +- Added report job persistence (`report_jobs`): + - workflow tracking + report type + request windows + status +- Added webhook completion handling: + - `eats.report.success` marks job completed by `job_id` + - persists report sections metadata for downstream download workers +- Existing CSV fetch route retained: + - `POST /api/v1/uber/reporting/fetch` for direct CSV pulls where applicable + +## Pending + +- Worker queue for downloading and processing each `report_metadata.sections[]` URL. +- Retry orchestration for section URL fetches and long-running reconciliation pipelines. diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index 423971a..2193e36 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -1174,6 +1174,19 @@ } } }, + "/api/v1/uber/reporting/create": { + "post": { + "summary": "Create Marketplace Reporting API job (POST /v1/eats/report)", + "tags": [ + "Uber Reporting" + ], + "responses": { + "200": { + "description": "Report creation requested successfully (workflow_id returned)" + } + } + } + }, "/api/v1/webhooks/uber": { "post": { "summary": "Ingest Uber webhook events", diff --git a/postman/Uber_Wrapper.postman_collection.json b/postman/Uber_Wrapper.postman_collection.json index 040c9a0..d5d627c 100644 --- a/postman/Uber_Wrapper.postman_collection.json +++ b/postman/Uber_Wrapper.postman_collection.json @@ -1531,6 +1531,39 @@ } } }, + { + "name": "Create Marketplace Report Job", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"report_type\": \"ORDERS_AND_ITEMS_REPORT\",\n \"store_uuids\": [\"{{storeId}}\"],\n \"start_date\": \"2026-03-01\",\n \"end_date\": \"2026-03-10\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/v1/uber/reporting/create", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "reporting", + "create" + ] + } + } + }, { "name": "Set Holiday Hours", "request": { @@ -1753,6 +1786,38 @@ } } }, + { + "name": "Webhook Ingest - Report Success (Simulation)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "X-Uber-Signature", + "value": "replace-with-valid-hmac" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"event_type\": \"eats.report.success\",\n \"event_id\": \"cd14f0bb-2d8c-44fb-9622-f6a4be18773e_2f7a1bdd-7993-4485-a11b-eacadce96b67\",\n \"job_id\": \"c7e05234-04ca-4460-8b03-d587df71228e\",\n \"report_type\": \"ORDERS_AND_ITEMS_REPORT\",\n \"start_time_ms\": 1742169600000,\n \"end_time_ms\": 1743119999000,\n \"report_metadata\": {\n \"sections\": []\n },\n \"webhook_meta\": {\n \"client_id\": \"ndkjscgfS5bvdiuyhv84sdhviudn\",\n \"webhook_config_id\": \"restaurant-financial-data.road-report-completion\",\n \"webhook_msg_timestamp\": 1743119999000,\n \"webhook_msg_uuid\": \"cd14f0bb-2d8c-44fb-9622-f6a4be18773e\"\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/adapter.js b/src/db/adapter.js index d652ef1..9cb4c28 100644 --- a/src/db/adapter.js +++ b/src/db/adapter.js @@ -11,5 +11,6 @@ module.exports = { webhookRepository: repositories.webhookRepository, apiLogRepository: repositories.apiLogRepository, appTokenRepository: repositories.appTokenRepository, - tokenRequestLogRepository: repositories.tokenRequestLogRepository + tokenRequestLogRepository: repositories.tokenRequestLogRepository, + reportJobRepository: repositories.reportJobRepository }; diff --git a/src/db/repositories.js b/src/db/repositories.js index 059fda2..eae8bb3 100644 --- a/src/db/repositories.js +++ b/src/db/repositories.js @@ -459,11 +459,148 @@ const tokenRequestLogRepository = { } }; +const reportJobRepository = { + createRequested({ + merchantId, + workflowId, + reportType, + storeUuids, + groupUuids, + startDate, + endDate + }) { + const existing = this.findByWorkflowId(workflowId); + const timestamp = nowIso(); + const row = { + id: existing?.id || uuidv4(), + merchant_id: merchantId || existing?.merchant_id || null, + workflow_id: workflowId, + report_type: reportType, + store_uuids_json: JSON.stringify(storeUuids || []), + group_uuids_json: JSON.stringify(groupUuids || []), + start_date: startDate, + end_date: endDate, + status: existing?.status === "completed" ? "completed" : "requested", + webhook_event_id: existing?.webhook_event_id || null, + webhook_msg_uuid: existing?.webhook_msg_uuid || null, + report_sections_json: existing?.report_sections_json || null, + webhook_payload_json: existing?.webhook_payload_json || null, + created_at: existing?.created_at || timestamp, + completed_at: existing?.completed_at || null, + updated_at: timestamp + }; + + db.prepare( + ` + INSERT INTO report_jobs ( + id, merchant_id, workflow_id, report_type, store_uuids_json, group_uuids_json, + start_date, end_date, status, webhook_event_id, webhook_msg_uuid, report_sections_json, + webhook_payload_json, created_at, completed_at, updated_at + ) + VALUES ( + @id, @merchant_id, @workflow_id, @report_type, @store_uuids_json, @group_uuids_json, + @start_date, @end_date, @status, @webhook_event_id, @webhook_msg_uuid, @report_sections_json, + @webhook_payload_json, @created_at, @completed_at, @updated_at + ) + ON CONFLICT(workflow_id) DO UPDATE SET + merchant_id = excluded.merchant_id, + report_type = excluded.report_type, + store_uuids_json = excluded.store_uuids_json, + group_uuids_json = excluded.group_uuids_json, + start_date = excluded.start_date, + end_date = excluded.end_date, + updated_at = excluded.updated_at + ` + ).run(row); + + return this.findByWorkflowId(workflowId); + }, + + markSuccessFromWebhook({ + workflowId, + eventId, + webhookMsgUuid, + reportType, + sections, + payload + }) { + const existing = this.findByWorkflowId(workflowId); + const timestamp = nowIso(); + + if (!existing) { + const row = { + id: uuidv4(), + merchant_id: null, + workflow_id: workflowId, + report_type: reportType || "UNKNOWN", + store_uuids_json: JSON.stringify([]), + group_uuids_json: JSON.stringify([]), + start_date: "", + end_date: "", + status: "completed", + webhook_event_id: eventId || null, + webhook_msg_uuid: webhookMsgUuid || null, + report_sections_json: JSON.stringify(sections || []), + webhook_payload_json: JSON.stringify(payload || {}), + created_at: timestamp, + completed_at: timestamp, + updated_at: timestamp + }; + db.prepare( + ` + INSERT INTO report_jobs ( + id, merchant_id, workflow_id, report_type, store_uuids_json, group_uuids_json, + start_date, end_date, status, webhook_event_id, webhook_msg_uuid, report_sections_json, + webhook_payload_json, created_at, completed_at, updated_at + ) + VALUES ( + @id, @merchant_id, @workflow_id, @report_type, @store_uuids_json, @group_uuids_json, + @start_date, @end_date, @status, @webhook_event_id, @webhook_msg_uuid, @report_sections_json, + @webhook_payload_json, @created_at, @completed_at, @updated_at + ) + ` + ).run(row); + return this.findByWorkflowId(workflowId); + } + + db.prepare( + ` + UPDATE report_jobs + SET status = 'completed', + report_type = COALESCE(?, report_type), + webhook_event_id = ?, + webhook_msg_uuid = ?, + report_sections_json = ?, + webhook_payload_json = ?, + completed_at = ?, + updated_at = ? + WHERE workflow_id = ? + ` + ).run( + reportType || null, + eventId || null, + webhookMsgUuid || null, + JSON.stringify(sections || []), + JSON.stringify(payload || {}), + timestamp, + timestamp, + workflowId + ); + + return this.findByWorkflowId(workflowId); + }, + + findByWorkflowId(workflowId) { + return db.prepare("SELECT * FROM report_jobs WHERE workflow_id = ? LIMIT 1").get(workflowId); + } +}; + module.exports = { merchantRepository, uberConnectionRepository, webhookRepository, apiLogRepository, appTokenRepository, - tokenRequestLogRepository + tokenRequestLogRepository, + reportJobRepository }; diff --git a/src/db/sqlite.js b/src/db/sqlite.js index bf44c5f..3787557 100644 --- a/src/db/sqlite.js +++ b/src/db/sqlite.js @@ -97,6 +97,26 @@ function initSchema() { grant_type TEXT NOT NULL, requested_at TEXT NOT NULL ); + + CREATE TABLE IF NOT EXISTS report_jobs ( + id TEXT PRIMARY KEY, + merchant_id TEXT, + workflow_id TEXT NOT NULL UNIQUE, + report_type TEXT NOT NULL, + store_uuids_json TEXT, + group_uuids_json TEXT, + start_date TEXT NOT NULL, + end_date TEXT NOT NULL, + status TEXT NOT NULL, + webhook_event_id TEXT, + webhook_msg_uuid TEXT, + report_sections_json TEXT, + webhook_payload_json TEXT, + created_at TEXT NOT NULL, + completed_at TEXT, + updated_at TEXT NOT NULL, + FOREIGN KEY(merchant_id) REFERENCES merchants(id) + ); `); if (!tableHasColumn("webhook_events", "resource_id")) { @@ -130,6 +150,19 @@ function initSchema() { if (!tableHasColumn("uber_connections", "last_webhook_environment")) { db.exec("ALTER TABLE uber_connections ADD COLUMN last_webhook_environment TEXT"); } + + if (!tableHasColumn("report_jobs", "webhook_event_id")) { + db.exec("ALTER TABLE report_jobs ADD COLUMN webhook_event_id TEXT"); + } + if (!tableHasColumn("report_jobs", "webhook_msg_uuid")) { + db.exec("ALTER TABLE report_jobs ADD COLUMN webhook_msg_uuid TEXT"); + } + if (!tableHasColumn("report_jobs", "report_sections_json")) { + db.exec("ALTER TABLE report_jobs ADD COLUMN report_sections_json TEXT"); + } + if (!tableHasColumn("report_jobs", "webhook_payload_json")) { + db.exec("ALTER TABLE report_jobs ADD COLUMN webhook_payload_json TEXT"); + } } module.exports = { diff --git a/src/modules/reporting/reporting.controller.js b/src/modules/reporting/reporting.controller.js index ff48ce8..ebdd8f8 100644 --- a/src/modules/reporting/reporting.controller.js +++ b/src/modules/reporting/reporting.controller.js @@ -1,5 +1,34 @@ const { z } = require("zod"); -const { fetchReport } = require("./reporting.service"); +const { fetchReport, createReportJob } = require("./reporting.service"); + +const REPORT_TYPES = [ + "PAYMENT_DETAILS_REPORT", + "ORDER_ERRORS_MENU_ITEM_REPORT", + "ORDER_ERRORS_TRANSACTION_REPORT", + "ORDER_HISTORY_REPORT", + "DOWNTIME_REPORT", + "CUSTOMER_AND_DELIVERY_FEEDBACK_REPORT", + "MENU_ITEM_FEEDBACK_REPORT", + "BILLING_DETAILS_REPORT", + "ORDERS_AND_ITEMS_REPORT", + "FINANCE_SUMMARY_REPORT" +]; + +const RANGE_LIMIT_DAYS = { + PAYMENT_DETAILS_REPORT: 30, + ORDERS_AND_ITEMS_REPORT: 15, + FINANCE_SUMMARY_REPORT: 30 +}; + +const LOOKBACK_RULES = { + ORDER_ERRORS_MENU_ITEM_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 }, + ORDER_ERRORS_TRANSACTION_REPORT: { minDaysAgo: 190, maxDaysAgo: 4 }, + ORDER_HISTORY_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 }, + DOWNTIME_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 }, + CUSTOMER_AND_DELIVERY_FEEDBACK_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 }, + MENU_ITEM_FEEDBACK_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 }, + BILLING_DETAILS_REPORT: { minDaysAgo: 1825, maxDaysAgo: 2 } +}; const fetchSchema = z.object({ method: z.enum(["GET", "POST"]).default("GET"), @@ -28,7 +57,109 @@ async function fetchReportingCsv(req, res) { }); } -module.exports = { - fetchReportingCsv -}; +function parseDateLike(input, allowDateTime) { + const raw = String(input || ""); + if (allowDateTime) { + if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) { + return new Date(`${raw}T00:00:00.000Z`); + } + return new Date(raw); + } + if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) { + return null; + } + return new Date(`${raw}T00:00:00.000Z`); +} +function daysBetweenInclusive(start, end) { + const msPerDay = 24 * 60 * 60 * 1000; + return Math.floor((end.getTime() - start.getTime()) / msPerDay) + 1; +} + +function daysAgo(date) { + const now = new Date(); + const utcNow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); + const utcDate = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + const msPerDay = 24 * 60 * 60 * 1000; + return Math.floor((utcNow.getTime() - utcDate.getTime()) / msPerDay); +} + +const createReportSchema = z + .object({ + merchantId: z.string().min(1).optional(), + report_type: z.enum(REPORT_TYPES), + store_uuids: z.array(z.string().uuid()).optional(), + group_uuids: z.array(z.string().uuid()).optional(), + start_date: z.string().min(1), + end_date: z.string().min(1) + }) + .refine((value) => (value.store_uuids?.length || 0) + (value.group_uuids?.length || 0) > 0, { + message: "At least one store_uuids or group_uuids value is required." + }); + +function validateCreateReportConstraints(payload) { + const allowDateTime = payload.report_type === "PAYMENT_DETAILS_REPORT"; + const start = parseDateLike(payload.start_date, allowDateTime); + const end = parseDateLike(payload.end_date, allowDateTime); + if (!start || Number.isNaN(start.getTime()) || !end || Number.isNaN(end.getTime())) { + const error = new Error("Invalid report date format for selected report_type."); + error.status = 400; + throw error; + } + if (start.getTime() > end.getTime()) { + const error = new Error("start_date must be on or before end_date."); + error.status = 414; + throw error; + } + + const rangeLimit = RANGE_LIMIT_DAYS[payload.report_type]; + if (rangeLimit) { + const rangeDays = daysBetweenInclusive(start, end); + if (rangeDays > rangeLimit) { + const error = new Error( + `${payload.report_type} supports a maximum range period of ${rangeLimit} days.` + ); + error.status = 416; + throw error; + } + } + + const lookback = LOOKBACK_RULES[payload.report_type]; + if (lookback) { + const startAgo = daysAgo(start); + const endAgo = daysAgo(end); + const startOutOfWindow = startAgo < lookback.maxDaysAgo || startAgo > lookback.minDaysAgo; + const endOutOfWindow = endAgo < lookback.maxDaysAgo || endAgo > lookback.minDaysAgo; + if (startOutOfWindow || endOutOfWindow) { + const error = new Error( + `${payload.report_type} must be within [T-${lookback.minDaysAgo}, T-${lookback.maxDaysAgo}] days.` + ); + error.status = 416; + throw error; + } + } +} + +async function createMarketplaceReport(req, res) { + const payload = createReportSchema.parse(req.body || {}); + validateCreateReportConstraints(payload); + + const data = await createReportJob({ + merchantId: payload.merchantId || null, + reportType: payload.report_type, + storeUuids: payload.store_uuids || [], + groupUuids: payload.group_uuids || [], + startDate: payload.start_date, + endDate: payload.end_date + }); + + return res.json({ + success: true, + data + }); +} + +module.exports = { + fetchReportingCsv, + createMarketplaceReport +}; diff --git a/src/modules/reporting/reporting.service.js b/src/modules/reporting/reporting.service.js index e816686..1b42836 100644 --- a/src/modules/reporting/reporting.service.js +++ b/src/modules/reporting/reporting.service.js @@ -1,6 +1,6 @@ const axios = require("axios"); const env = require("../../config/env"); -const { apiLogRepository } = require("../../db/adapter"); +const { apiLogRepository, reportJobRepository } = require("../../db/adapter"); const { getCachedClientCredentialsToken, AUTH_SCOPES } = require("../auth/auth.service"); const { normalizeUberError, isRetryableUberError } = require("../common/errors/uberError"); const { withExponentialBackoffRetry } = require("../common/http/retry"); @@ -155,6 +155,93 @@ async function fetchReport({ } module.exports = { + async createReportJob({ + merchantId, + reportType, + storeUuids, + groupUuids, + startDate, + endDate + }) { + const token = await getCachedClientCredentialsToken({ + scope: AUTH_SCOPES.REPORT + }); + + const body = { + report_type: reportType, + store_uuids: storeUuids || [], + group_uuids: groupUuids || [], + start_date: startDate, + end_date: endDate + }; + + try { + const response = await withExponentialBackoffRetry({ + fn: async () => + reportingClient.request({ + method: "POST", + url: "/v1/eats/report", + data: body, + headers: { + Authorization: buildAuthorizationHeader(token.token_type, token.access_token), + "Content-Type": "application/json" + } + }), + maxAttempts: 4, + baseDelayMs: 400, + shouldRetry: (error) => isRetryableUberError(error) + }); + + const workflowId = response?.data?.workflow_id; + if (!workflowId) { + const error = new Error("Missing workflow_id in report creation response."); + error.status = 502; + throw error; + } + + reportJobRepository.createRequested({ + merchantId, + workflowId, + reportType, + storeUuids, + groupUuids, + startDate, + endDate + }); + + apiLogRepository.insert({ + merchantId, + method: "POST", + wrapperRoute: "/api/v1/uber/reporting/create", + uberPath: "/v1/eats/report", + responseStatus: response.status, + requestBody: body, + responseBody: response.data + }); + + return { + workflow_id: workflowId + }; + } catch (error) { + const normalized = normalizeUberError(error); + apiLogRepository.insert({ + merchantId, + method: "POST", + wrapperRoute: "/api/v1/uber/reporting/create", + uberPath: "/v1/eats/report", + responseStatus: normalized.status, + requestBody: body, + responseBody: { + code: normalized.code, + message: normalized.message, + transient: normalized.transient, + details: + typeof normalized.details === "string" ? { raw: normalized.details } : normalized.details + } + }); + throw normalized; + } + }, fetchReport, parseCsvByHeader }; diff --git a/src/modules/webhooks/webhooks.controller.js b/src/modules/webhooks/webhooks.controller.js index 799056d..e38621c 100644 --- a/src/modules/webhooks/webhooks.controller.js +++ b/src/modules/webhooks/webhooks.controller.js @@ -1,6 +1,10 @@ const crypto = require("crypto"); const env = require("../../config/env"); -const { webhookRepository, uberConnectionRepository } = require("../../db/adapter"); +const { + webhookRepository, + uberConnectionRepository, + reportJobRepository +} = require("../../db/adapter"); function getSignatureFromHeaders(headers) { const signature = headers["x-uber-signature"]; @@ -104,6 +108,26 @@ function applyMenuRefreshRequestFromWebhook(eventType, payload, headers) { }); } +function applyReportSuccessFromWebhook(eventType, payload) { + if (eventType !== "eats.report.success") { + return; + } + + const workflowId = payload?.job_id; + if (!workflowId) { + return; + } + + reportJobRepository.markSuccessFromWebhook({ + workflowId: String(workflowId), + eventId: payload?.event_id || null, + webhookMsgUuid: payload?.webhook_meta?.webhook_msg_uuid || null, + reportType: payload?.report_type || null, + sections: payload?.report_metadata?.sections || [], + payload + }); +} + async function handleUberWebhook(req, res) { if (!verifyBasicAuthIfConfigured(req)) { return res.status(401).json({ @@ -151,6 +175,7 @@ async function handleUberWebhook(req, res) { applyProvisioningStateFromWebhook(eventType, req.body || {}); applyMenuRefreshRequestFromWebhook(eventType, req.body || {}, req.headers || {}); + applyReportSuccessFromWebhook(eventType, req.body || {}); return res.status(200).end(); } diff --git a/src/routes/reporting.routes.js b/src/routes/reporting.routes.js index 2cdf8fc..18a8a62 100644 --- a/src/routes/reporting.routes.js +++ b/src/routes/reporting.routes.js @@ -1,6 +1,9 @@ const express = require("express"); const asyncHandler = require("../middleware/asyncHandler"); -const { fetchReportingCsv } = require("../modules/reporting/reporting.controller"); +const { + fetchReportingCsv, + createMarketplaceReport +} = require("../modules/reporting/reporting.controller"); const router = express.Router(); @@ -17,5 +20,17 @@ const router = express.Router(); */ router.post("/uber/reporting/fetch", asyncHandler(fetchReportingCsv)); -module.exports = router; +/** + * @openapi + * /api/v1/uber/reporting/create: + * post: + * summary: Create Marketplace Reporting API job (POST /v1/eats/report) + * tags: + * - Uber Reporting + * responses: + * 200: + * description: Report creation requested successfully (workflow_id returned) + */ +router.post("/uber/reporting/create", asyncHandler(createMarketplaceReport)); +module.exports = router;