const axios = require("axios"); const env = require("../../config/env"); const uberEndpoints = require("../../config/uberEndpoints"); const { uberConnectionRepository, apiLogRepository } = require("../../db/adapter"); const { getCachedClientCredentialsToken, AUTH_SCOPES } = 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 }); 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 }; } async function callUberApi({ merchantId, method, uberPath, query, body, wrapperRoute, authMode, scopes, headers }) { const resolvedAuth = await resolveAuthToken({ authMode, merchantId, scopes }); try { const response = await 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) }); apiLogRepository.insert({ merchantId, method, wrapperRoute, uberPath, responseStatus: response.status, requestBody: body, responseBody: response.data }); return response.data; } catch (error) { const normalized = normalizeUberError(error); apiLogRepository.insert({ merchantId, method, wrapperRoute, uberPath, responseStatus: normalized.status, requestBody: 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.upsert, { storeId }); return callUberApi({ merchantId, method: "PUT", uberPath, body: payload, 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, payload }) { const uberPath = interpolatePath(uberEndpoints.menu.itemsUpdate, { storeId }); 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 };