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 };