Implement Marketplace Reporting API create flow and success webhook tracking
This commit is contained in:
parent
8ba675e45a
commit
6ff3d800f4
@ -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
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ Common event types handled:
|
||||
- `store.deprovisioned`
|
||||
- `store.menu_refresh_request`
|
||||
- `store.status.changed`
|
||||
- `eats.report.success`
|
||||
|
||||
Menu refresh handling:
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
28
docs/developer-portal/20-marketplace-reporting-api-audit.md
Normal file
28
docs/developer-portal/20-marketplace-reporting-api-audit.md
Normal file
@ -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.
|
||||
@ -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",
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -11,5 +11,6 @@ module.exports = {
|
||||
webhookRepository: repositories.webhookRepository,
|
||||
apiLogRepository: repositories.apiLogRepository,
|
||||
appTokenRepository: repositories.appTokenRepository,
|
||||
tokenRequestLogRepository: repositories.tokenRequestLogRepository
|
||||
tokenRequestLogRepository: repositories.tokenRequestLogRepository,
|
||||
reportJobRepository: repositories.reportJobRepository
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user