UBER-EATS-Wrapper/src/modules/proxy/proxy.service.js

737 lines
20 KiB
JavaScript

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 } = 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
});
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
};
}
async function callUberApi({
merchantId,
method,
uberPath,
query,
body,
logRequestBody,
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: 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, 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
};