diff --git a/docs/developer-portal/00-marketplace-overview-audit.md b/docs/developer-portal/00-marketplace-overview-audit.md new file mode 100644 index 0000000..162b23b --- /dev/null +++ b/docs/developer-portal/00-marketplace-overview-audit.md @@ -0,0 +1,54 @@ +# 00 Marketplace Overview Audit + +Source checked: Uber Eats Marketplace APIs Overview section shared by you. + +## Status Matrix (Doc vs Current Code) + +- Integration Configuration: + Implemented partly. Merchant and OAuth connection setup exists. + Files: `src/modules/connections`, `src/modules/auth`. + Pending: typed onboarding endpoints from official Integration Configuration docs. + +- Menu: + Implemented partly. + Implemented: menu upsert/get wrappers. + Files: `src/modules/proxy/proxy.service.js`, `src/routes/proxy.routes.js`. + Pending: full endpoint set from official Menu docs. + +- Store: + Implemented partly. + Implemented: update store hours shortcut. + Pending: full store details/status/holiday hours typed routes. + +- Order: + Implemented partly. + Implemented: list orders and order actions (accept/deny/ready/cancel). + Pending: all order lifecycle endpoints from official Order docs, including detailed states and BYOC-specific flows. + +- Promotions: + Not implemented as typed APIs yet. + Temporary coverage: generic passthrough endpoint. + +- Reporting: + Not implemented as typed APIs yet. + Temporary coverage: generic passthrough endpoint. + +- Webhooks: + Implemented basic ingestion and persistence. + Pending: signature verification, idempotency key enforcement, async processing pipeline. + +## Production Requirement Alignment + +- 99% injection success tracking: + Implemented initial metric endpoint: + `GET /api/v1/metrics/injection-success` + based on logged responses for `accept_pos_order`. + +## Important Current Constraint + +Until endpoint-specific docs are provided, the wrapper supports full coverage through: + +- `POST /api/v1/uber/request` + +Typed endpoint expansion will continue section-by-section as you send official docs. + diff --git a/docs/developer-portal/11-production-go-live.md b/docs/developer-portal/11-production-go-live.md index 9277664..a5963ea 100644 --- a/docs/developer-portal/11-production-go-live.md +++ b/docs/developer-portal/11-production-go-live.md @@ -6,4 +6,4 @@ Go-live readiness: - Webhook URL reachable over HTTPS - Alerting and log monitoring enabled - Token refresh and failure runbooks ready - +- Injection success rate monitoring enabled (`target >= 99%`) diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index ad054e5..ed3a53c 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -145,6 +145,37 @@ } } }, + "/api/v1/metrics/injection-success": { + "get": { + "summary": "Get order injection success metric", + "tags": [ + "Metrics" + ], + "parameters": [ + { + "in": "query", + "name": "merchantId", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "windowDays", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Injection metrics" + } + } + } + }, "/api/v1/uber/request": { "post": { "summary": "Generic Uber passthrough for any Uber endpoint", diff --git a/src/app.js b/src/app.js index 7f48274..ade1c4b 100644 --- a/src/app.js +++ b/src/app.js @@ -12,6 +12,7 @@ const authRoutes = require("./routes/auth.routes"); const connectionRoutes = require("./routes/connections.routes"); const proxyRoutes = require("./routes/proxy.routes"); const webhookRoutes = require("./routes/webhooks.routes"); +const metricsRoutes = require("./routes/metrics.routes"); const app = express(); @@ -25,10 +26,10 @@ app.use("/health", healthRoutes); app.use("/api/v1/auth", requireWrapperApiKey, authRoutes); app.use("/api/v1", requireWrapperApiKey, connectionRoutes); app.use("/api/v1", requireWrapperApiKey, proxyRoutes); +app.use("/api/v1", requireWrapperApiKey, metricsRoutes); app.use("/api/v1", webhookRoutes); app.use("/docs", swaggerUi.serve, swaggerUi.setup(spec)); app.use(errorHandler); module.exports = app; - diff --git a/src/db/repositories.js b/src/db/repositories.js index 1c92f42..72eb22f 100644 --- a/src/db/repositories.js +++ b/src/db/repositories.js @@ -148,6 +148,46 @@ const apiLogRepository = { `); stmt.run(row); return row; + }, + + getInjectionSuccessStats({ merchantId, sinceIso }) { + const params = []; + const filters = ["uber_path LIKE '%/accept_pos_order%'"]; + + if (merchantId) { + filters.push("merchant_id = ?"); + params.push(merchantId); + } + + if (sinceIso) { + filters.push("created_at >= ?"); + params.push(sinceIso); + } + + const whereSql = filters.length ? `WHERE ${filters.join(" AND ")}` : ""; + + const totals = db + .prepare( + ` + SELECT + COUNT(*) AS total, + SUM(CASE WHEN response_status >= 200 AND response_status < 300 THEN 1 ELSE 0 END) AS success + FROM api_logs + ${whereSql} + ` + ) + .get(...params); + + const total = totals?.total || 0; + const success = totals?.success || 0; + const successRate = total === 0 ? 0 : Number(((success / total) * 100).toFixed(2)); + + return { + total, + success, + failed: total - success, + successRate + }; } }; @@ -157,4 +197,3 @@ module.exports = { webhookRepository, apiLogRepository }; - diff --git a/src/modules/metrics/metrics.controller.js b/src/modules/metrics/metrics.controller.js new file mode 100644 index 0000000..b80b172 --- /dev/null +++ b/src/modules/metrics/metrics.controller.js @@ -0,0 +1,44 @@ +const { z } = require("zod"); +const { apiLogRepository } = require("../../db/adapter"); + +function toSinceIso(windowDays) { + if (!windowDays) { + return null; + } + const days = Number(windowDays); + if (Number.isNaN(days) || days <= 0) { + return null; + } + const ms = days * 24 * 60 * 60 * 1000; + return new Date(Date.now() - ms).toISOString(); +} + +async function getInjectionSuccess(req, res) { + const schema = z.object({ + merchantId: z.string().optional(), + windowDays: z.string().optional() + }); + + const query = schema.parse(req.query); + const sinceIso = toSinceIso(query.windowDays); + const stats = apiLogRepository.getInjectionSuccessStats({ + merchantId: query.merchantId, + sinceIso + }); + + return res.json({ + success: true, + data: { + metric: "order_injection_success_rate", + merchantId: query.merchantId || "all", + windowDays: query.windowDays ? Number(query.windowDays) : "all_time", + targetPercent: 99, + ...stats + } + }); +} + +module.exports = { + getInjectionSuccess +}; + diff --git a/src/routes/metrics.routes.js b/src/routes/metrics.routes.js new file mode 100644 index 0000000..55b5b72 --- /dev/null +++ b/src/routes/metrics.routes.js @@ -0,0 +1,32 @@ +const express = require("express"); +const asyncHandler = require("../middleware/asyncHandler"); +const { getInjectionSuccess } = require("../modules/metrics/metrics.controller"); + +const router = express.Router(); + +/** + * @openapi + * /api/v1/metrics/injection-success: + * get: + * summary: Get order injection success metric + * tags: + * - Metrics + * parameters: + * - in: query + * name: merchantId + * required: false + * schema: + * type: string + * - in: query + * name: windowDays + * required: false + * schema: + * type: integer + * responses: + * 200: + * description: Injection metrics + */ +router.get("/metrics/injection-success", asyncHandler(getInjectionSuccess)); + +module.exports = router; +