feat: add marketplace overview audit and injection success metrics endpoint
This commit is contained in:
parent
7de04ab4e0
commit
156f7cdd58
54
docs/developer-portal/00-marketplace-overview-audit.md
Normal file
54
docs/developer-portal/00-marketplace-overview-audit.md
Normal file
@ -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.
|
||||
|
||||
@ -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%`)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
44
src/modules/metrics/metrics.controller.js
Normal file
44
src/modules/metrics/metrics.controller.js
Normal file
@ -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
|
||||
};
|
||||
|
||||
32
src/routes/metrics.routes.js
Normal file
32
src/routes/metrics.routes.js
Normal file
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user