From 0c41ad5858f62a5aee8c9c77e33200068a832f63 Mon Sep 17 00:00:00 2001 From: MOHAN Date: Sun, 29 Mar 2026 18:31:05 +0530 Subject: [PATCH] feat: implement promotions api v1.0.0 create revoke get and list endpoints --- docs/developer-portal/02-api-groups.md | 2 +- .../17-promotions-api-1-0-0-audit.md | 32 +++++ docs/developer-portal/17-promotions.md | 14 ++ docs/openapi/openapi.json | 90 +++++++++++++ postman/Uber_Wrapper.postman_collection.json | 121 ++++++++++++++++++ src/config/uberEndpoints.js | 6 + src/modules/proxy/proxy.controller.js | 63 ++++++++- src/modules/proxy/proxy.service.js | 53 +++++++- src/routes/proxy.routes.js | 83 ++++++++++++ 9 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 docs/developer-portal/17-promotions-api-1-0-0-audit.md create mode 100644 docs/developer-portal/17-promotions.md diff --git a/docs/developer-portal/02-api-groups.md b/docs/developer-portal/02-api-groups.md index 02692fa..74fa4e9 100644 --- a/docs/developer-portal/02-api-groups.md +++ b/docs/developer-portal/02-api-groups.md @@ -14,7 +14,7 @@ This file intentionally separates high-priority vs extended APIs for easier team ## Group B (Extended / Optional / Can Be Added Incrementally) -- Promotions +- Promotions (Promotions API v1.0.0 route set available) - Ads / sponsored listings - Payout and financial reconciliation (Reporting API route now available) - Store holiday/special schedules diff --git a/docs/developer-portal/17-promotions-api-1-0-0-audit.md b/docs/developer-portal/17-promotions-api-1-0-0-audit.md new file mode 100644 index 0000000..7ce3ff0 --- /dev/null +++ b/docs/developer-portal/17-promotions-api-1-0-0-audit.md @@ -0,0 +1,32 @@ +# 17 Promotions API 1.0.0 Audit + +Source checked: "Uber Eats Marketplace Promotions API (1.0.0)" shared by you. + +## Implemented Now (Dedicated Wrapper Namespace) + +- Create Promotion: + - `POST /api/v1/uber/delivery-promotions/stores/{storeId}` + - upstream: `/v1/delivery/stores/{store_id}/promotion` +- Revoke Promotion: + - `POST /api/v1/uber/delivery-promotions/{promotionId}/revoke` + - upstream: `/v1/delivery/promotions/{promotion_id}/revoke` +- Get Promotion: + - `GET /api/v1/uber/delivery-promotions/{promotionId}` + - upstream: `/v1/delivery/promotions/{promotion_id}` +- Get Promotions: + - `GET /api/v1/uber/delivery-promotions/stores/{storeId}` + - upstream: `/v1/delivery/stores/{store_id}/promotions` + +## Validation Added + +- `user_group` enum: `ALL_CUSTOMERS | FIRST_TIME_CUSTOMERS` +- promotions list `state` enum: + - `active`, `pending`, `completed`, `revoked`, `expired`, `deleted` +- required top-level create payload fields enforced + +## Pending + +- Strict schema unions for each promotion type object variant: + - flat_off, free_item, bogo, percent_off, menu_item, free_delivery +- typed handling for `time_range` object query form when full exact query contract is shared + diff --git a/docs/developer-portal/17-promotions.md b/docs/developer-portal/17-promotions.md new file mode 100644 index 0000000..6730409 --- /dev/null +++ b/docs/developer-portal/17-promotions.md @@ -0,0 +1,14 @@ +# 17 Promotions + +Promotions API 1.0.0 wrapper routes: + +- `POST /api/v1/uber/delivery-promotions/stores/{storeId}` (Create promotion) +- `GET /api/v1/uber/delivery-promotions/stores/{storeId}` (List promotions) +- `GET /api/v1/uber/delivery-promotions/{promotionId}` (Get single promotion) +- `POST /api/v1/uber/delivery-promotions/{promotionId}/revoke` (Revoke promotion) + +Notes: + +- Only promotions created via API are expected to be returned by get/list endpoints. +- User group and state filters are validated. + diff --git a/docs/openapi/openapi.json b/docs/openapi/openapi.json index 6a06089..6410153 100644 --- a/docs/openapi/openapi.json +++ b/docs/openapi/openapi.json @@ -1026,6 +1026,96 @@ } } }, + "/api/v1/uber/delivery-promotions/stores/{storeId}": { + "post": { + "summary": "Promotions API 1.0.0 - Create promotion", + "tags": [ + "Uber Delivery Promotions v1" + ], + "parameters": [ + { + "in": "path", + "name": "storeId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Promotion created" + } + } + }, + "get": { + "summary": "Promotions API 1.0.0 - Get promotions by store", + "tags": [ + "Uber Delivery Promotions v1" + ], + "parameters": [ + { + "in": "path", + "name": "storeId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Promotions listed" + } + } + } + }, + "/api/v1/uber/delivery-promotions/{promotionId}": { + "get": { + "summary": "Promotions API 1.0.0 - Get promotion by ID", + "tags": [ + "Uber Delivery Promotions v1" + ], + "parameters": [ + { + "in": "path", + "name": "promotionId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Promotion retrieved" + } + } + } + }, + "/api/v1/uber/delivery-promotions/{promotionId}/revoke": { + "post": { + "summary": "Promotions API 1.0.0 - Revoke promotion", + "tags": [ + "Uber Delivery Promotions v1" + ], + "parameters": [ + { + "in": "path", + "name": "promotionId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Promotion revoked" + } + } + } + }, "/api/v1/uber/reporting/fetch": { "post": { "summary": "Fetch Uber reporting CSV with retries and header-based parsing", diff --git a/postman/Uber_Wrapper.postman_collection.json b/postman/Uber_Wrapper.postman_collection.json index 1a42297..a49c242 100644 --- a/postman/Uber_Wrapper.postman_collection.json +++ b/postman/Uber_Wrapper.postman_collection.json @@ -927,6 +927,123 @@ } } }, + { + "name": "Promotions API - Create Promotion", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"start_time\": \"2026-04-10T00:00:00-07:00\",\n \"end_time\": \"2026-04-20T00:00:00-07:00\",\n \"external_promotion_id\": \"Uber_Superbowl\",\n \"user_group\": \"ALL_CUSTOMERS\",\n \"allow_unlimited_apply\": true,\n \"currency_code\": \"USD\",\n \"budget\": {\n \"unlimited_budget\": true\n },\n \"promo_type\": \"FLATOFF\",\n \"promotion_discount\": {\n \"flat_off_discount\": {}\n }\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/v1/uber/delivery-promotions/stores/{{storeId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "delivery-promotions", + "stores", + "{{storeId}}" + ] + } + } + }, + { + "name": "Promotions API - List Promotions", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/uber/delivery-promotions/stores/{{storeId}}?state=active", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "delivery-promotions", + "stores", + "{{storeId}}" + ], + "query": [ + { + "key": "state", + "value": "active" + } + ] + } + } + }, + { + "name": "Promotions API - Get Promotion", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/uber/delivery-promotions/{{promotionId}}", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "delivery-promotions", + "{{promotionId}}" + ] + } + } + }, + { + "name": "Promotions API - Revoke Promotion", + "request": { + "method": "POST", + "header": [ + { + "key": "x-api-key", + "value": "{{apiKey}}" + } + ], + "url": { + "raw": "{{baseUrl}}/api/v1/uber/delivery-promotions/{{promotionId}}/revoke", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "v1", + "uber", + "delivery-promotions", + "{{promotionId}}", + "revoke" + ] + } + } + }, { "name": "Get Store By ID", "request": { @@ -1491,6 +1608,10 @@ { "key": "orderId", "value": "" + }, + { + "key": "promotionId", + "value": "" } ] } diff --git a/src/config/uberEndpoints.js b/src/config/uberEndpoints.js index 1465f51..a090dc9 100644 --- a/src/config/uberEndpoints.js +++ b/src/config/uberEndpoints.js @@ -49,6 +49,12 @@ module.exports = { deliveryByoc: { ingestCourierLocation: "/v1/eats/byoc/restaurants/orders/event/location" }, + deliveryPromotions: { + createByStore: "/v1/delivery/stores/{storeId}/promotion", + revokeById: "/v1/delivery/promotions/{promotionId}/revoke", + getById: "/v1/delivery/promotions/{promotionId}", + listByStore: "/v1/delivery/stores/{storeId}/promotions" + }, webhooks: { events: "/v1/eats/stores/{storeId}/event_feed" } diff --git a/src/modules/proxy/proxy.controller.js b/src/modules/proxy/proxy.controller.js index 747bc26..4773ab3 100644 --- a/src/modules/proxy/proxy.controller.js +++ b/src/modules/proxy/proxy.controller.js @@ -635,6 +635,63 @@ async function deliveryByocIngestCourierLocation(req, res) { return res.json({ success: true, data }); } +async function deliveryCreatePromotion(req, res) { + const budgetSchema = z + .object({ + unlimited_budget: z.boolean().optional(), + amount_e5: z.coerce.number().optional() + }) + .optional(); + + const promotionSchema = z.object({ + start_time: z.string(), + end_time: z.string(), + external_promotion_id: z.string().optional(), + user_group: z.enum(["ALL_CUSTOMERS", "FIRST_TIME_CUSTOMERS"]).optional(), + allow_unlimited_apply: z.boolean().optional(), + currency_code: z.string().optional(), + budget: budgetSchema, + promo_type: z.string(), + promotion_discount: z.record(z.string(), z.any()).optional(), + promotion_customization: z.record(z.string(), z.any()).optional() + }); + + const payload = promotionSchema.parse(req.body || {}); + const data = await proxyService.deliveryCreatePromotion({ + storeId: req.params.storeId, + payload + }); + return res.json({ success: true, data }); +} + +async function deliveryRevokePromotion(req, res) { + const data = await proxyService.deliveryRevokePromotion({ + promotionId: req.params.promotionId + }); + return res.json({ success: true, data }); +} + +async function deliveryGetPromotion(req, res) { + const data = await proxyService.deliveryGetPromotion({ + promotionId: req.params.promotionId + }); + return res.json({ success: true, data }); +} + +async function deliveryListPromotions(req, res) { + const schema = z.object({ + state: z.enum(["active", "pending", "completed", "revoked", "expired", "deleted"]).optional(), + start_time: z.string().optional(), + end_time: z.string().optional() + }); + const query = schema.parse(req.query || {}); + const data = await proxyService.deliveryListPromotions({ + storeId: req.params.storeId, + query + }); + return res.json({ success: true, data }); +} + module.exports = { genericProxy, upsertMenu, @@ -676,5 +733,9 @@ module.exports = { deliveryResolveFulfillmentIssues, deliveryGetReplacementRecommendations, deliveryUpdatePartnerCount, - deliveryByocIngestCourierLocation + deliveryByocIngestCourierLocation, + deliveryCreatePromotion, + deliveryRevokePromotion, + deliveryGetPromotion, + deliveryListPromotions }; diff --git a/src/modules/proxy/proxy.service.js b/src/modules/proxy/proxy.service.js index 79fe16e..17e11de 100644 --- a/src/modules/proxy/proxy.service.js +++ b/src/modules/proxy/proxy.service.js @@ -612,6 +612,53 @@ async function deliveryByocIngestCourierLocation({ payload }) { }); } +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, @@ -653,5 +700,9 @@ module.exports = { deliveryResolveFulfillmentIssues, deliveryGetReplacementRecommendations, deliveryUpdatePartnerCount, - deliveryByocIngestCourierLocation + deliveryByocIngestCourierLocation, + deliveryCreatePromotion, + deliveryRevokePromotion, + deliveryGetPromotion, + deliveryListPromotions }; diff --git a/src/routes/proxy.routes.js b/src/routes/proxy.routes.js index a893366..3ec2c71 100644 --- a/src/routes/proxy.routes.js +++ b/src/routes/proxy.routes.js @@ -688,4 +688,87 @@ router.post( asyncHandler(controller.deliveryByocIngestCourierLocation) ); +/** + * @openapi + * /api/v1/uber/delivery-promotions/stores/{storeId}: + * post: + * summary: Promotions API 1.0.0 - Create promotion + * tags: + * - Uber Delivery Promotions v1 + * parameters: + * - in: path + * name: storeId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Promotion created + * get: + * summary: Promotions API 1.0.0 - Get promotions by store + * tags: + * - Uber Delivery Promotions v1 + * parameters: + * - in: path + * name: storeId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Promotions listed + */ +router.post( + "/uber/delivery-promotions/stores/:storeId", + asyncHandler(controller.deliveryCreatePromotion) +); +router.get( + "/uber/delivery-promotions/stores/:storeId", + asyncHandler(controller.deliveryListPromotions) +); + +/** + * @openapi + * /api/v1/uber/delivery-promotions/{promotionId}: + * get: + * summary: Promotions API 1.0.0 - Get promotion by ID + * tags: + * - Uber Delivery Promotions v1 + * parameters: + * - in: path + * name: promotionId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Promotion retrieved + */ +router.get( + "/uber/delivery-promotions/:promotionId", + asyncHandler(controller.deliveryGetPromotion) +); + +/** + * @openapi + * /api/v1/uber/delivery-promotions/{promotionId}/revoke: + * post: + * summary: Promotions API 1.0.0 - Revoke promotion + * tags: + * - Uber Delivery Promotions v1 + * parameters: + * - in: path + * name: promotionId + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Promotion revoked + */ +router.post( + "/uber/delivery-promotions/:promotionId/revoke", + asyncHandler(controller.deliveryRevokePromotion) +); + module.exports = router;