UBER-EATS-Wrapper/src/db/repositories.js

445 lines
12 KiB
JavaScript

const { v4: uuidv4 } = require("uuid");
const { db } = require("./sqlite");
function nowIso() {
return new Date().toISOString();
}
function extractOrderIdFromPath(uberPath) {
if (!uberPath) {
return null;
}
const match = uberPath.match(/\/orders\/([^/]+)/i);
return match ? decodeURIComponent(match[1]) : null;
}
const merchantRepository = {
upsert({ id, name, externalRef }) {
const timestamp = nowIso();
const merchantId = id || uuidv4();
const stmt = db.prepare(`
INSERT INTO merchants (id, name, external_ref, created_at, updated_at)
VALUES (@id, @name, @external_ref, @created_at, @updated_at)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
external_ref = excluded.external_ref,
updated_at = excluded.updated_at
`);
stmt.run({
id: merchantId,
name,
external_ref: externalRef || null,
created_at: timestamp,
updated_at: timestamp
});
return this.findById(merchantId);
},
findById(id) {
return db.prepare("SELECT * FROM merchants WHERE id = ?").get(id);
},
list() {
return db.prepare("SELECT * FROM merchants ORDER BY created_at DESC").all();
}
};
const uberConnectionRepository = {
upsertByMerchantId(merchantId, payload) {
const existing = this.findByMerchantId(merchantId);
const timestamp = nowIso();
const row = {
id: existing?.id || uuidv4(),
merchant_id: merchantId,
uber_user_id: payload.uberUserId || existing?.uber_user_id || null,
uber_store_id: payload.uberStoreId || existing?.uber_store_id || null,
access_token: payload.accessToken || existing?.access_token,
refresh_token: payload.refreshToken ?? existing?.refresh_token ?? null,
token_type: payload.tokenType ?? existing?.token_type ?? "Bearer",
scope: payload.scope ?? existing?.scope ?? null,
expires_at: payload.expiresAt ?? existing?.expires_at ?? null,
status: payload.status ?? existing?.status ?? "active",
created_at: existing?.created_at || timestamp,
updated_at: timestamp
};
const stmt = db.prepare(`
INSERT INTO uber_connections (
id, merchant_id, uber_user_id, uber_store_id, access_token, refresh_token,
token_type, scope, expires_at, status, created_at, updated_at
)
VALUES (
@id, @merchant_id, @uber_user_id, @uber_store_id, @access_token, @refresh_token,
@token_type, @scope, @expires_at, @status, @created_at, @updated_at
)
ON CONFLICT(merchant_id) DO UPDATE SET
uber_user_id = excluded.uber_user_id,
uber_store_id = excluded.uber_store_id,
access_token = excluded.access_token,
refresh_token = excluded.refresh_token,
token_type = excluded.token_type,
scope = excluded.scope,
expires_at = excluded.expires_at,
status = excluded.status,
updated_at = excluded.updated_at
`);
stmt.run(row);
return this.findByMerchantId(merchantId);
},
findByMerchantId(merchantId) {
return db.prepare("SELECT * FROM uber_connections WHERE merchant_id = ?").get(merchantId);
},
findByUberStoreId(uberStoreId) {
return db.prepare("SELECT * FROM uber_connections WHERE uber_store_id = ? LIMIT 1").get(uberStoreId);
},
setStatusByMerchantId(merchantId, status) {
const timestamp = nowIso();
db.prepare(
`
UPDATE uber_connections
SET status = ?, updated_at = ?
WHERE merchant_id = ?
`
).run(status, timestamp, merchantId);
return this.findByMerchantId(merchantId);
},
list() {
return db
.prepare(`
SELECT uc.*, m.name AS merchant_name
FROM uber_connections uc
JOIN merchants m ON m.id = uc.merchant_id
ORDER BY uc.created_at DESC
`)
.all();
}
};
const webhookRepository = {
findByDedupeKey(dedupeKey) {
if (!dedupeKey) {
return null;
}
return db.prepare("SELECT * FROM webhook_events WHERE dedupe_key = ? LIMIT 1").get(dedupeKey);
},
insert({
provider,
merchantId,
eventType,
resourceId,
resourceHref,
uberSignature,
dedupeKey,
payloadJson,
headersJson
}) {
const row = {
id: uuidv4(),
provider,
merchant_id: merchantId || null,
event_type: eventType || null,
resource_id: resourceId || null,
resource_href: resourceHref || null,
uber_signature: uberSignature || null,
dedupe_key: dedupeKey || null,
payload_json: JSON.stringify(payloadJson || {}),
headers_json: JSON.stringify(headersJson || {}),
received_at: nowIso(),
processing_status: "received"
};
const stmt = db.prepare(`
INSERT INTO webhook_events (
id, provider, merchant_id, event_type, resource_id, resource_href, uber_signature, dedupe_key,
payload_json, headers_json,
received_at, processing_status
)
VALUES (
@id, @provider, @merchant_id, @event_type, @resource_id, @resource_href, @uber_signature, @dedupe_key,
@payload_json, @headers_json,
@received_at, @processing_status
)
`);
stmt.run(row);
return row;
}
};
const apiLogRepository = {
insert({ merchantId, method, wrapperRoute, uberPath, responseStatus, requestBody, responseBody }) {
const orderId = extractOrderIdFromPath(uberPath);
const row = {
id: uuidv4(),
merchant_id: merchantId || null,
order_id: orderId,
http_method: method,
wrapper_route: wrapperRoute,
uber_path: uberPath,
response_status: responseStatus,
request_json: JSON.stringify(requestBody || {}),
response_json: JSON.stringify(responseBody || {}),
created_at: nowIso()
};
const stmt = db.prepare(`
INSERT INTO api_logs (
id, merchant_id, order_id, http_method, wrapper_route, uber_path,
response_status, request_json, response_json, created_at
)
VALUES (
@id, @merchant_id, @order_id, @http_method, @wrapper_route, @uber_path,
@response_status, @request_json, @response_json, @created_at
)
`);
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
};
},
getOrderResponseSlaStats({ merchantId, sinceIso }) {
const webhookFilters = [
"provider = 'uber'",
"(event_type = 'orders.notification' OR event_type = 'orders.scheduled.notification')",
"resource_id IS NOT NULL"
];
const webhookParams = [];
if (merchantId) {
webhookFilters.push("merchant_id = ?");
webhookParams.push(merchantId);
}
if (sinceIso) {
webhookFilters.push("received_at >= ?");
webhookParams.push(sinceIso);
}
const webhookRows = db
.prepare(
`
SELECT resource_id, merchant_id, received_at
FROM webhook_events
WHERE ${webhookFilters.join(" AND ")}
ORDER BY received_at ASC
`
)
.all(...webhookParams);
const uniqueOrders = new Map();
webhookRows.forEach((row) => {
if (!uniqueOrders.has(row.resource_id)) {
uniqueOrders.set(row.resource_id, row);
}
});
let withinSla = 0;
let responded = 0;
uniqueOrders.forEach((row, orderId) => {
const action = db
.prepare(
`
SELECT created_at
FROM api_logs
WHERE order_id = ?
AND (
uber_path LIKE '%/accept_pos_order'
OR uber_path LIKE '%/deny_pos_order'
)
AND response_status >= 200
AND response_status < 300
ORDER BY created_at ASC
LIMIT 1
`
)
.get(orderId);
if (!action) {
return;
}
responded += 1;
const receivedAtMs = new Date(row.received_at).getTime();
const actionAtMs = new Date(action.created_at).getTime();
const elapsedMs = actionAtMs - receivedAtMs;
if (!Number.isNaN(elapsedMs) && elapsedMs <= 11.5 * 60 * 1000) {
withinSla += 1;
}
});
const totalNotifications = uniqueOrders.size;
const withinSlaRate =
totalNotifications === 0 ? 0 : Number(((withinSla / totalNotifications) * 100).toFixed(2));
return {
totalNotifications,
responded,
noResponseYet: totalNotifications - responded,
withinSla,
breachedSla: responded - withinSla,
withinSlaRate
};
}
};
const appTokenRepository = {
findValid({ provider, grantType, scope, minValiditySeconds = 120 }) {
const row = db
.prepare(
`
SELECT *
FROM app_tokens
WHERE provider = ?
AND grant_type = ?
AND scope = ?
LIMIT 1
`
)
.get(provider, grantType, scope);
if (!row) {
return null;
}
const minExpiry = Date.now() + minValiditySeconds * 1000;
const expiresAtMs = new Date(row.expires_at).getTime();
if (Number.isNaN(expiresAtMs) || expiresAtMs <= minExpiry) {
return null;
}
return row;
},
upsert({ provider, grantType, scope, accessToken, tokenType, expiresAt }) {
const existing = db
.prepare(
`
SELECT * FROM app_tokens
WHERE provider = ? AND grant_type = ? AND scope = ?
LIMIT 1
`
)
.get(provider, grantType, scope);
const timestamp = nowIso();
const row = {
id: existing?.id || uuidv4(),
provider,
grant_type: grantType,
scope,
access_token: accessToken,
token_type: tokenType || "Bearer",
expires_at: expiresAt,
created_at: existing?.created_at || timestamp,
updated_at: timestamp
};
db.prepare(
`
INSERT INTO app_tokens (
id, provider, grant_type, scope, access_token, token_type, expires_at, created_at, updated_at
)
VALUES (
@id, @provider, @grant_type, @scope, @access_token, @token_type, @expires_at, @created_at, @updated_at
)
ON CONFLICT(provider, grant_type, scope) DO UPDATE SET
access_token = excluded.access_token,
token_type = excluded.token_type,
expires_at = excluded.expires_at,
updated_at = excluded.updated_at
`
).run(row);
return db
.prepare(
`
SELECT *
FROM app_tokens
WHERE provider = ? AND grant_type = ? AND scope = ?
LIMIT 1
`
)
.get(provider, grantType, scope);
}
};
const tokenRequestLogRepository = {
insert({ provider, grantType }) {
db.prepare(
`
INSERT INTO token_request_logs (
id, provider, grant_type, requested_at
)
VALUES (?, ?, ?, ?)
`
).run(uuidv4(), provider, grantType, nowIso());
},
countInLastHour({ provider, grantType }) {
const sinceIso = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const row = db
.prepare(
`
SELECT COUNT(*) AS total
FROM token_request_logs
WHERE provider = ?
AND grant_type = ?
AND requested_at >= ?
`
)
.get(provider, grantType, sinceIso);
return row?.total || 0;
}
};
module.exports = {
merchantRepository,
uberConnectionRepository,
webhookRepository,
apiLogRepository,
appTokenRepository,
tokenRequestLogRepository
};