const axios = require("axios"); const zlib = require("zlib"); const { promisify } = require("util"); const env = require("../../config/env"); const uberEndpoints = require("../../config/uberEndpoints"); const { uberConnectionRepository, apiLogRepository, appTokenRepository } = require("../../db/adapter"); const { getCachedClientCredentialsToken, refreshToken, AUTH_SCOPES, AUTH_GRANT_TYPES } = require("../auth/auth.service"); const { normalizeUberError, isRetryableUberError } = require("../common/errors/uberError"); const { withExponentialBackoffRetry } = require("../common/http/retry"); const uberApiClient = axios.create({ baseURL: env.UBER_API_BASE_URL, timeout: 30000 }); const gzipAsync = promisify(zlib.gzip); function interpolatePath(pathTemplate, params = {}) { let output = pathTemplate; Object.entries(params).forEach(([key, value]) => { output = output.replaceAll(`{${key}}`, encodeURIComponent(value)); }); return output; } function buildAuthHeader(tokenType, accessToken) { return `${tokenType || "Bearer"} ${accessToken}`; } function getMerchantConnectionToken(merchantId) { const connection = uberConnectionRepository.findByMerchantId(merchantId); if (!connection || connection.status !== "active") { const error = new Error("Active Uber merchant OAuth connection not found"); error.status = 404; throw error; } return { tokenType: connection.token_type || "Bearer", accessToken: connection.access_token }; } async function resolveAuthToken({ authMode = "app", merchantId, scopes }) { if (authMode === "merchant") { if (!merchantId) { const error = new Error("merchantId is required when authMode=merchant"); error.status = 400; throw error; } return getMerchantConnectionToken(merchantId); } const tokenData = await getCachedClientCredentialsToken({ scope: scopes || AUTH_SCOPES.ORDER }); return { tokenType: tokenData.token_type || "Bearer", accessToken: tokenData.access_token }; } function isUnauthorizedError(error) { return Number(error?.response?.status || 0) === 401; } async function refreshMerchantConnectionToken(merchantId) { const connection = uberConnectionRepository.findByMerchantId(merchantId); if (!connection) { const error = new Error("Uber merchant connection not found for token refresh."); error.status = 404; throw error; } if (!connection.refresh_token) { const error = new Error("Refresh token is not available for merchant OAuth connection."); error.status = 401; throw error; } const tokenData = await refreshToken(connection.refresh_token); const expiresAt = tokenData.expires_in ? new Date(Date.now() + Number(tokenData.expires_in) * 1000).toISOString() : connection.expires_at; const updated = uberConnectionRepository.upsertByMerchantId(merchantId, { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token || connection.refresh_token, tokenType: tokenData.token_type || connection.token_type || "Bearer", scope: tokenData.scope || connection.scope || null, expiresAt, status: "active" }); return { tokenType: updated.token_type || "Bearer", accessToken: updated.access_token }; } async function callUberApi({ merchantId, method, uberPath, query, body, logRequestBody, wrapperRoute, authMode, scopes, headers }) { const scopeKey = scopes || AUTH_SCOPES.ORDER; const normalizedScope = scopeKey.trim().split(/\s+/).sort().join(" "); let resolvedAuth = await resolveAuthToken({ authMode, merchantId, scopes: normalizedScope }); const buildRequest = () => withExponentialBackoffRetry({ fn: async () => uberApiClient.request({ method, url: uberPath, params: query, data: body, headers: { Authorization: buildAuthHeader(resolvedAuth.tokenType, resolvedAuth.accessToken), "Content-Type": "application/json", ...(headers || {}) } }), maxAttempts: 4, baseDelayMs: 300, shouldRetry: (error) => isRetryableUberError(error) }); try { let response; try { response = await buildRequest(); } catch (error) { if (!isUnauthorizedError(error)) { throw error; } if (authMode === "merchant" && merchantId) { resolvedAuth = await refreshMerchantConnectionToken(merchantId); } else { appTokenRepository.deleteByProviderGrantScope({ provider: "uber", grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS, scope: normalizedScope }); const freshToken = await getCachedClientCredentialsToken({ scope: normalizedScope, forceRefresh: true }); resolvedAuth = { tokenType: freshToken.token_type || "Bearer", accessToken: freshToken.access_token }; } response = await buildRequest(); } apiLogRepository.insert({ merchantId, method, wrapperRoute, uberPath, responseStatus: response.status, requestBody: logRequestBody !== undefined ? logRequestBody : body, responseBody: response.data }); return response.data; } catch (error) { const normalized = normalizeUberError(error); apiLogRepository.insert({ merchantId, method, wrapperRoute, uberPath, responseStatus: normalized.status, requestBody: logRequestBody !== undefined ? logRequestBody : body, responseBody: { code: normalized.code, message: normalized.message, transient: normalized.transient, details: normalized.details } }); throw normalized; } } async function genericProxy({ merchantId, method, path, query, body, authMode = "app", scopes }) { return callUberApi({ merchantId, method, uberPath: path, query, body, wrapperRoute: "/api/v1/uber/request", authMode, scopes }); } async function menuUpsert({ merchantId, storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.menu.upsert, { storeId }); return callUberApi({ merchantId, method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/menu/upsert", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function menuReplace({ merchantId, storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.menu.upload, { storeId }); const compressedPayload = await gzipAsync(Buffer.from(JSON.stringify(payload), "utf8")); return callUberApi({ merchantId, method: "PUT", uberPath, body: compressedPayload, logRequestBody: payload, headers: { "Content-Encoding": "gzip", "Content-Type": "application/json" }, wrapperRoute: "/api/v1/uber/menu/replace", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function menuGet({ merchantId, storeId, menuType }) { const uberPath = interpolatePath(uberEndpoints.menu.get, { storeId }); return callUberApi({ merchantId, method: "GET", uberPath, query: { menu_type: menuType || "MENU_TYPE_FULFILLMENT_DELIVERY" }, headers: { "Accept-Encoding": "gzip" }, wrapperRoute: "/api/v1/uber/menu", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function updateMenuItems({ merchantId, storeId, itemId, payload }) { const uberPath = interpolatePath(uberEndpoints.menu.itemUpdate, { storeId, itemId }); return callUberApi({ merchantId, method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/menu/items", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function ordersList({ merchantId, storeId, query }) { const uberPath = interpolatePath(uberEndpoints.orders.list, { storeId }); return callUberApi({ merchantId, method: "GET", uberPath, query, wrapperRoute: "/api/v1/uber/orders", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function getOrderById({ orderId }) { const uberPath = interpolatePath(uberEndpoints.orders.getById, { orderId }); return callUberApi({ method: "GET", uberPath, wrapperRoute: "/api/v1/uber/orders/:orderId", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function orderAction({ merchantId, orderId, action, payload }) { const routeMap = { resolve: uberEndpoints.orders.resolveFulfillmentIssue, accept: uberEndpoints.orders.accept, deny: uberEndpoints.orders.deny, ready: uberEndpoints.orders.readyForPickup, cancel: uberEndpoints.orders.cancel }; const template = routeMap[action]; if (!template) { const error = new Error("Unsupported order action"); error.status = 400; throw error; } const uberPath = interpolatePath(template, { orderId }); return callUberApi({ merchantId, method: "POST", uberPath, body: payload || {}, wrapperRoute: "/api/v1/uber/orders/:orderId/action", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function resolveFulfillmentIssues({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.orders.resolveFulfillmentIssue, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/orders/:orderId/fulfillment-issues", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function markOrderReady({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.orders.readyForPickup, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload || {}, wrapperRoute: "/api/v1/uber/orders/:orderId/ready", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function updateStoreHours({ merchantId, storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.stores.updateHours, { storeId }); return callUberApi({ merchantId, method: "PUT", uberPath, body: payload, wrapperRoute: "/api/v1/uber/stores/hours", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function listProvisionableStores({ merchantId, query }) { return callUberApi({ merchantId, method: "GET", uberPath: uberEndpoints.stores.list, query, wrapperRoute: "/api/v1/uber/stores/provisionable", authMode: "merchant", scopes: AUTH_SCOPES.POS_PROVISIONING }); } async function listStores({ query }) { return callUberApi({ method: "GET", uberPath: uberEndpoints.stores.list, query, wrapperRoute: "/api/v1/uber/stores", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function getStoreById({ storeId }) { const uberPath = interpolatePath(uberEndpoints.stores.getById, { storeId }); return callUberApi({ method: "GET", uberPath, wrapperRoute: "/api/v1/uber/stores/:storeId", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function getStoreStatus({ storeId }) { const uberPath = interpolatePath(uberEndpoints.stores.status, { storeId }); return callUberApi({ method: "GET", uberPath, wrapperRoute: "/api/v1/uber/stores/:storeId/status", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function setStoreStatus({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.stores.status, { storeId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/stores/:storeId/status", authMode: "app", scopes: AUTH_SCOPES.STORE_STATUS_WRITE }); } async function getHolidayHours({ storeId }) { const uberPath = interpolatePath(uberEndpoints.stores.holidayHours, { storeId }); return callUberApi({ method: "GET", uberPath, wrapperRoute: "/api/v1/uber/stores/:storeId/holiday-hours", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function setHolidayHours({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.stores.holidayHours, { storeId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/stores/:storeId/holiday-hours", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function createPosData({ merchantId, storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.stores.posData, { storeId }); return callUberApi({ merchantId, method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/stores/:storeId/pos-data", authMode: "merchant", scopes: AUTH_SCOPES.POS_PROVISIONING }); } async function getPosData({ storeId }) { const uberPath = interpolatePath(uberEndpoints.stores.posData, { storeId }); return callUberApi({ method: "GET", uberPath, wrapperRoute: "/api/v1/uber/stores/:storeId/pos-data", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function patchPosData({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.stores.posData, { storeId }); return callUberApi({ method: "PATCH", uberPath, body: payload, wrapperRoute: "/api/v1/uber/stores/:storeId/pos-data", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deletePosData({ storeId }) { const uberPath = interpolatePath(uberEndpoints.stores.posData, { storeId }); return callUberApi({ method: "DELETE", uberPath, wrapperRoute: "/api/v1/uber/stores/:storeId/pos-data", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deliveryListStores({ query }) { return callUberApi({ method: "GET", uberPath: uberEndpoints.deliveryStore.list, query, wrapperRoute: "/api/v1/uber/delivery-store/stores", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deliveryGetStoreDetails({ storeId, query }) { const uberPath = interpolatePath(uberEndpoints.deliveryStore.getById, { storeId }); return callUberApi({ method: "GET", uberPath, query, wrapperRoute: "/api/v1/uber/delivery-store/stores/:storeId", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deliveryUpdateStore({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryStore.update, { storeId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-store/stores/:storeId", authMode: "app", scopes: AUTH_SCOPES.STORE_WRITE }); } async function deliveryGetStoreStatus({ storeId }) { const uberPath = interpolatePath(uberEndpoints.deliveryStore.getStatus, { storeId }); return callUberApi({ method: "GET", uberPath, wrapperRoute: "/api/v1/uber/delivery-store/stores/:storeId/status", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deliverySetStoreStatus({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryStore.setStatus, { storeId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-store/stores/:storeId/status", authMode: "app", scopes: AUTH_SCOPES.STORE_STATUS_WRITE }); } async function deliveryUpdatePrepTime({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryStore.updatePrepTime, { storeId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-store/stores/:storeId/prep-time", authMode: "app", scopes: AUTH_SCOPES.STORE_WRITE }); } async function deliveryUpdateFulfillmentConfig({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryStore.updateFulfillmentConfig, { storeId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-store/stores/:storeId/fulfillment-configuration", authMode: "app", scopes: AUTH_SCOPES.BYOC_FULFILLMENT_CONFIG }); } async function deliveryGetOrderDetails({ orderId, query }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.getById, { orderId }); return callUberApi({ method: "GET", uberPath, query, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryListOrders({ storeId, query }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.listByStore, { storeId }); return callUberApi({ method: "GET", uberPath, query, wrapperRoute: "/api/v1/uber/delivery-order/stores/:storeId/orders", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryAcceptOrder({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.accept, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload || {}, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId/accept", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryDenyOrder({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.deny, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId/deny", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryCancelOrder({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.cancel, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId/cancel", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryMarkOrderReady({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.ready, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload || {}, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId/ready", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryAdjustOrderPrice({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.adjustPrice, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId/adjust-price", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryUpdateReadyTime({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.updateReadyTime, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId/update-ready-time", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryResolveFulfillmentIssues({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryOrder.resolveFulfillmentIssues, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-order/orders/:orderId/resolve-fulfillment-issues", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryGetReplacementRecommendations({ payload }) { return callUberApi({ method: "POST", uberPath: uberEndpoints.deliveryOrder.replacementRecommendations, body: payload, wrapperRoute: "/api/v1/uber/delivery-order/replacement-recommendations", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryUpdatePartnerCount({ orderId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryPartner.updateDeliveryPartnerCount, { orderId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-partner/orders/:orderId/partner-count", authMode: "app", scopes: AUTH_SCOPES.ORDER }); } async function deliveryByocIngestCourierLocation({ payload }) { return callUberApi({ method: "POST", uberPath: uberEndpoints.deliveryByoc.ingestCourierLocation, body: payload, wrapperRoute: "/api/v1/uber/delivery-byoc/courier-location", authMode: "app", scopes: AUTH_SCOPES.BYOC_FULFILLMENT_CONFIG }); } async function deliveryCreatePromotion({ storeId, payload }) { const uberPath = interpolatePath(uberEndpoints.deliveryPromotions.createByStore, { storeId }); return callUberApi({ method: "POST", uberPath, body: payload, wrapperRoute: "/api/v1/uber/delivery-promotions/stores/:storeId", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deliveryRevokePromotion({ promotionId }) { const uberPath = interpolatePath(uberEndpoints.deliveryPromotions.revokeById, { promotionId }); return callUberApi({ method: "POST", uberPath, body: {}, wrapperRoute: "/api/v1/uber/delivery-promotions/:promotionId/revoke", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deliveryGetPromotion({ promotionId }) { const uberPath = interpolatePath(uberEndpoints.deliveryPromotions.getById, { promotionId }); return callUberApi({ method: "GET", uberPath, wrapperRoute: "/api/v1/uber/delivery-promotions/:promotionId", authMode: "app", scopes: AUTH_SCOPES.STORE }); } async function deliveryListPromotions({ storeId, query }) { const uberPath = interpolatePath(uberEndpoints.deliveryPromotions.listByStore, { storeId }); return callUberApi({ method: "GET", uberPath, query, wrapperRoute: "/api/v1/uber/delivery-promotions/stores/:storeId", authMode: "app", scopes: AUTH_SCOPES.STORE }); } module.exports = { genericProxy, menuUpsert, menuReplace, menuGet, updateMenuItems, ordersList, getOrderById, orderAction, resolveFulfillmentIssues, markOrderReady, updateStoreHours, listProvisionableStores, listStores, getStoreById, getStoreStatus, setStoreStatus, getHolidayHours, setHolidayHours, createPosData, getPosData, patchPosData, deletePosData, deliveryListStores, deliveryGetStoreDetails, deliveryUpdateStore, deliveryGetStoreStatus, deliverySetStoreStatus, deliveryUpdatePrepTime, deliveryUpdateFulfillmentConfig, deliveryGetOrderDetails, deliveryListOrders, deliveryAcceptOrder, deliveryDenyOrder, deliveryCancelOrder, deliveryMarkOrderReady, deliveryAdjustOrderPrice, deliveryUpdateReadyTime, deliveryResolveFulfillmentIssues, deliveryGetReplacementRecommendations, deliveryUpdatePartnerCount, deliveryByocIngestCourierLocation, deliveryCreatePromotion, deliveryRevokePromotion, deliveryGetPromotion, deliveryListPromotions };