diff --git a/docs/developer-portal/03-integration-configuration-flows-audit.md b/docs/developer-portal/03-integration-configuration-flows-audit.md index 21b092d..ff7725f 100644 --- a/docs/developer-portal/03-integration-configuration-flows-audit.md +++ b/docs/developer-portal/03-integration-configuration-flows-audit.md @@ -9,6 +9,8 @@ Source checked: Uber Eats "Integration Configuration Flows" section shared by yo - Uses merchant token (`authorization_code` / `eats.pos_provisioning`) - Store activation via POS data: - `POST /api/v1/uber/stores/{storeId}/pos-data` +- Retrieve integration configuration: + - `GET /api/v1/uber/stores/{storeId}/pos-data` - Store integration update/deactivation: - `PATCH /api/v1/uber/stores/{storeId}/pos-data` - Store de-provision: @@ -26,6 +28,5 @@ Source checked: Uber Eats "Integration Configuration Flows" section shared by yo ## Pending - Strong store mapping workflow (location-data assisted matching UI flow) -- Typed POS data schema once exact request fields are finalized from endpoint reference +- Strict nested schema validation for deeper webhook config object variants - Automated post-provisioning follow-up actions (e.g., mandatory menu upload jobs) - diff --git a/docs/developer-portal/03-integration-configuration-suite-1-0-0-audit.md b/docs/developer-portal/03-integration-configuration-suite-1-0-0-audit.md new file mode 100644 index 0000000..9c5fa99 --- /dev/null +++ b/docs/developer-portal/03-integration-configuration-suite-1-0-0-audit.md @@ -0,0 +1,36 @@ +# 03 Integration Configuration Suite 1.0.0 Audit + +Source checked: "Uber Eats Integration Activation & Configuration API Suite (1.0.0)" shared by you. + +## Endpoint Coverage + +- Activate Integration: + - `POST /api/v1/uber/stores/{storeId}/pos-data` + - Uses merchant OAuth token (`eats.pos_provisioning`) via `merchantId` +- Update Integration Configuration: + - `PATCH /api/v1/uber/stores/{storeId}/pos-data` + - Uses app token with `eats.store` scope +- Retrieve Integration Configuration: + - `GET /api/v1/uber/stores/{storeId}/pos-data` + - Uses app token with `eats.store` scope +- Remove Integration Configuration: + - `DELETE /api/v1/uber/stores/{storeId}/pos-data` + - Uses app token with `eats.store` scope + +## Schema Alignment + +- Added typed request schema fields for `pos_data`: + - `allowed_customer_requests` + - `integrator_brand_id` + - `integrator_store_id` + - `is_order_manager` + - `merchant_store_id` + - `require_manual_acceptance` + - `store_configuration_data` + - `webhooks_config` + - `integration_enabled` (PATCH) + +## Pending + +- Add stricter nested object typing for all `webhooks_config` variants when final examples are shared. + diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index e366405..b224264 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -629,6 +629,27 @@ } } }, + "get": { + "summary": "Retrieve integration configuration for selected store (GET /pos_data)", + "tags": [ + "Uber Provisioning" + ], + "parameters": [ + { + "in": "path", + "name": "storeId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Store integration configuration retrieved" + } + } + }, "patch": { "summary": "Update integration settings for selected store (PATCH /pos_data)", "tags": [ diff --git a/postman/Uber_Wrapper.postman_collection.json b/postman/Uber_Wrapper.postman_collection.json index afff804..6c04d0a 100644 --- a/postman/Uber_Wrapper.postman_collection.json +++ b/postman/Uber_Wrapper.postman_collection.json @@ -662,7 +662,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"posData\": {\n \"partner_store_id\": \"POS_STORE_001\",\n \"integration_enabled\": true,\n \"store_configuration_data\": {}\n }\n}" + "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"posData\": {\n \"allowed_customer_requests\": {\n \"allow_single_use_items_requests\": false,\n \"allow_special_instruction_requests\": false\n },\n \"integrator_brand_id\": \"app-brand-1jj9th32\",\n \"integrator_store_id\": \"app-store-001\",\n \"is_order_manager\": true,\n \"merchant_store_id\": \"UberStore1\",\n \"require_manual_acceptance\": false,\n \"store_configuration_data\": \"v1-config\",\n \"webhooks_config\": {\n \"webhooks_version\": \"1.0.0\"\n }\n }\n}" }, "url": { "raw": "{{baseUrl}}/api/v1/uber/stores/{{storeId}}/pos-data", @@ -680,6 +680,32 @@ } } }, + { + "name": "Get POS Data", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/uber/stores/{{storeId}}/pos-data", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "stores", + "{{storeId}}", + "pos-data" + ] + } + } + }, { "name": "Patch POS Data (Enable/Disable)", "request": { @@ -696,7 +722,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"posData\": {\n \"integration_enabled\": false\n }\n}" + "raw": "{\n \"posData\": {\n \"integration_enabled\": false,\n \"require_manual_acceptance\": false\n }\n}" }, "url": { "raw": "{{baseUrl}}/api/v1/uber/stores/{{storeId}}/pos-data", @@ -722,16 +748,8 @@ { "key": "x-api-key", "value": "{{apiKey}}" - }, - { - "key": "Content-Type", - "value": "application/json" } ], - "body": { - "mode": "raw", - "raw": "{\n \"merchantId\": \"{{merchantId}}\"\n}" - }, "url": { "raw": "{{baseUrl}}/api/v1/uber/stores/{{storeId}}/pos-data", "host": [ diff --git a/src/modules/proxy/proxy.controller.js b/src/modules/proxy/proxy.controller.js index ba52d3e..5661473 100644 --- a/src/modules/proxy/proxy.controller.js +++ b/src/modules/proxy/proxy.controller.js @@ -239,9 +239,37 @@ async function setHolidayHours(req, res) { } async function createPosData(req, res) { + const allowedCustomerRequestsSchema = z + .object({ + allow_single_use_items_requests: z.boolean().optional(), + allow_special_instruction_requests: z.boolean().optional() + }) + .optional(); + + const webhooksConfigSchema = z + .object({ + order_release_webhooks: z.record(z.string(), z.any()).optional(), + schedule_order_webhooks: z.record(z.string(), z.any()).optional(), + delivery_status_webhooks: z.record(z.string(), z.any()).optional(), + webhooks_version: z.string().optional() + }) + .optional(); + + const posDataSchema = z.object({ + allowed_customer_requests: allowedCustomerRequestsSchema, + integrator_brand_id: z.string().optional(), + integrator_store_id: z.string().optional(), + is_order_manager: z.boolean().optional(), + merchant_store_id: z.string().optional(), + require_manual_acceptance: z.boolean().optional(), + store_configuration_data: z.string().optional(), + webhooks_config: webhooksConfigSchema, + integration_enabled: z.boolean().optional() + }); + const schema = z.object({ merchantId: z.string().min(1), - posData: z.any() + posData: posDataSchema }); const payload = schema.parse(req.body); const data = await proxyService.createPosData({ @@ -252,14 +280,51 @@ async function createPosData(req, res) { return res.json({ success: true, data }); } +async function getPosData(req, res) { + const data = await proxyService.getPosData({ + storeId: req.params.storeId + }); + return res.json({ success: true, data }); +} + async function patchPosData(req, res) { + const allowedCustomerRequestsSchema = z + .object({ + allow_single_use_items_requests: z.boolean().optional(), + allow_special_instruction_requests: z.boolean().optional() + }) + .optional(); + + const webhooksConfigSchema = z + .object({ + order_release_webhooks: z.record(z.string(), z.any()).optional(), + schedule_order_webhooks: z.record(z.string(), z.any()).optional(), + delivery_status_webhooks: z.record(z.string(), z.any()).optional(), + webhooks_version: z.string().optional() + }) + .optional(); + + const posDataSchema = z + .object({ + allowed_customer_requests: allowedCustomerRequestsSchema, + integrator_brand_id: z.string().optional(), + integrator_store_id: z.string().optional(), + is_order_manager: z.boolean().optional(), + merchant_store_id: z.string().optional(), + require_manual_acceptance: z.boolean().optional(), + store_configuration_data: z.string().optional(), + webhooks_config: webhooksConfigSchema, + integration_enabled: z.boolean().optional() + }) + .refine((value) => Object.keys(value).length > 0, { + message: "posData must include at least one updatable field" + }); + const schema = z.object({ - merchantId: z.string().min(1), - posData: z.any() + posData: posDataSchema }); const payload = schema.parse(req.body); const data = await proxyService.patchPosData({ - merchantId: payload.merchantId, storeId: req.params.storeId, payload: payload.posData }); @@ -267,12 +332,7 @@ async function patchPosData(req, res) { } async function deletePosData(req, res) { - const schema = z.object({ - merchantId: z.string().min(1) - }); - const payload = schema.parse(req.body); const data = await proxyService.deletePosData({ - merchantId: payload.merchantId, storeId: req.params.storeId }); return res.json({ success: true, data }); @@ -298,6 +358,7 @@ module.exports = { getHolidayHours, setHolidayHours, createPosData, + getPosData, patchPosData, deletePosData }; diff --git a/src/modules/proxy/proxy.service.js b/src/modules/proxy/proxy.service.js index 658d69d..24c4771 100644 --- a/src/modules/proxy/proxy.service.js +++ b/src/modules/proxy/proxy.service.js @@ -354,28 +354,37 @@ async function createPosData({ merchantId, storeId, payload }) { }); } -async function patchPosData({ merchantId, storeId, payload }) { +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({ - merchantId, method: "PATCH", uberPath, body: payload, wrapperRoute: "/api/v1/uber/stores/:storeId/pos-data", - authMode: "merchant", - scopes: AUTH_SCOPES.POS_PROVISIONING + authMode: "app", + scopes: AUTH_SCOPES.STORE }); } -async function deletePosData({ merchantId, storeId }) { +async function deletePosData({ storeId }) { const uberPath = interpolatePath(uberEndpoints.stores.posData, { storeId }); return callUberApi({ - merchantId, method: "DELETE", uberPath, wrapperRoute: "/api/v1/uber/stores/:storeId/pos-data", - authMode: "merchant", - scopes: AUTH_SCOPES.POS_PROVISIONING + authMode: "app", + scopes: AUTH_SCOPES.STORE }); } @@ -399,6 +408,7 @@ module.exports = { getHolidayHours, setHolidayHours, createPosData, + getPosData, patchPosData, deletePosData }; diff --git a/src/routes/proxy.routes.js b/src/routes/proxy.routes.js index 3ce008e..a09454f 100644 --- a/src/routes/proxy.routes.js +++ b/src/routes/proxy.routes.js @@ -307,6 +307,19 @@ router.put("/uber/stores/hours", asyncHandler(controller.updateHours)); * responses: * 200: * description: Store provisioned + * get: + * summary: Retrieve integration configuration for selected store (GET /pos_data) + * tags: + * - Uber Provisioning + * parameters: + * - in: path + * name: storeId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Store integration configuration retrieved * patch: * summary: Update integration settings for selected store (PATCH /pos_data) * tags: @@ -335,6 +348,7 @@ router.put("/uber/stores/hours", asyncHandler(controller.updateHours)); * description: Store integration removed */ router.post("/uber/stores/:storeId/pos-data", asyncHandler(controller.createPosData)); +router.get("/uber/stores/:storeId/pos-data", asyncHandler(controller.getPosData)); router.patch("/uber/stores/:storeId/pos-data", asyncHandler(controller.patchPosData)); router.delete("/uber/stores/:storeId/pos-data", asyncHandler(controller.deletePosData));