445 lines
12 KiB
JavaScript
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
|
|
};
|