feat: add typed retail fulfillment issue and order-ready endpoints

This commit is contained in:
MOHAN 2026-03-29 18:01:17 +05:30
parent 6b768bfbf5
commit 8eb11897a5
8 changed files with 285 additions and 0 deletions

View File

@ -12,9 +12,17 @@ Order flow for POS:
Typed routes: Typed routes:
- `GET /api/v1/uber/orders/{orderId}` (order details) - `GET /api/v1/uber/orders/{orderId}` (order details)
- `POST /api/v1/uber/orders/{orderId}/fulfillment-issues`
- `POST /api/v1/uber/orders/{orderId}/ready`
- `POST /api/v1/uber/orders/{orderId}/action` with action: - `POST /api/v1/uber/orders/{orderId}/action` with action:
- `accept` - `accept`
- `deny` - `deny`
- `ready` - `ready`
- `cancel` - `cancel`
- `resolve` - `resolve`
Retail fulfillment guidance:
- Read customer preference (`REPLACE_FOR_ME`, `SUBSTITUTE_ME`, `REMOVE_ITEM`) from order details.
- Update issue states via fulfillment endpoint (`FOUND_ITEM`, `PARTIAL_AVAILABILITY`, `OUT_OF_ITEM`).
- On `orders.fulfillment_issues.resolved` webhook, fetch latest order and continue resolution.

View File

@ -0,0 +1,38 @@
# 06 Retail Fulfillment Audit
Source checked: Uber Eats "Retail Order Fulfillment API Guide" section shared by you.
## Implemented Now
- Dedicated fulfillment issues endpoint:
- `POST /api/v1/uber/orders/{orderId}/fulfillment-issues`
- Supported issue types in validation:
- `FOUND_ITEM`
- `PARTIAL_AVAILABILITY`
- `OUT_OF_ITEM`
- Supported action types in validation:
- `REPLACE_FOR_ME`
- `SUBSTITUTE_ME`
- `REMOVE_ITEM`
- `ALTERNATIVE_ITEM`
- `SUBSTITUTION_REJECTED`
- Dedicated order-ready endpoint:
- `POST /api/v1/uber/orders/{orderId}/ready`
- Existing webhook handling includes:
- `orders.fulfillment_issues.resolved` event ingestion + signature verification + dedupe
## Existing Before
- Get order details endpoint and order actions endpoint
- Webhook ingestion and `200` acknowledgement
- Resolve flow available through generic action path
## Pending
- Exact payload field-level schema parity for all quantity/weight variants from endpoint reference
- Automated workflow engine:
- fetch order after `orders.fulfillment_issues.resolved`
- inspect `customer_ack_type`
- trigger next fulfillment action automatically
- Explicit support helper for count-vs-weight conversion rules

View File

@ -25,3 +25,7 @@ Common event types handled:
- `store.provisioned` - `store.provisioned`
- `store.deprovisioned` - `store.deprovisioned`
- `store.status.changed` - `store.status.changed`
Retail fulfillment follow-up:
- On `orders.fulfillment_issues.resolved`, fetch updated order details and inspect customer acknowledgment before next action.

View File

@ -347,6 +347,52 @@
} }
} }
}, },
"/api/v1/uber/orders/{orderId}/fulfillment-issues": {
"post": {
"summary": "Resolve retail fulfillment issues (found/partial/out-of-stock substitutions)",
"tags": [
"Uber Orders"
],
"parameters": [
{
"in": "path",
"name": "orderId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Fulfillment issue update submitted"
}
}
}
},
"/api/v1/uber/orders/{orderId}/ready": {
"post": {
"summary": "Mark order ready for handoff/pickup",
"tags": [
"Uber Orders"
],
"parameters": [
{
"in": "path",
"name": "orderId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Order marked ready"
}
}
}
},
"/api/v1/uber/stores": { "/api/v1/uber/stores": {
"get": { "get": {
"summary": "Retrieve all stores provisioned to developer account", "summary": "Retrieve all stores provisioned to developer account",

View File

@ -457,6 +457,74 @@
} }
} }
}, },
{
"name": "Resolve Fulfillment Issues (Typed)",
"request": {
"method": "POST",
"header": [
{
"key": "x-api-key",
"value": "{{apiKey}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"issues\": [\n {\n \"cart_item_id\": \"cart_item_1\",\n \"issue_type\": \"OUT_OF_ITEM\",\n \"action_type\": \"REPLACE_FOR_ME\",\n \"item_substitute\": {\n \"id\": \"sub_item_1\"\n }\n }\n ]\n}"
},
"url": {
"raw": "{{baseUrl}}/api/v1/uber/orders/{{orderId}}/fulfillment-issues",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"v1",
"uber",
"orders",
"{{orderId}}",
"fulfillment-issues"
]
}
}
},
{
"name": "Mark Order Ready",
"request": {
"method": "POST",
"header": [
{
"key": "x-api-key",
"value": "{{apiKey}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{}"
},
"url": {
"raw": "{{baseUrl}}/api/v1/uber/orders/{{orderId}}/ready",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"v1",
"uber",
"orders",
"{{orderId}}",
"ready"
]
}
}
},
{ {
"name": "Order Response SLA Metric", "name": "Order Response SLA Metric",
"request": { "request": {

View File

@ -110,6 +110,58 @@ async function orderAction(req, res) {
return res.json({ success: true, data }); return res.json({ success: true, data });
} }
async function resolveFulfillmentIssues(req, res) {
const quantityObject = z
.object({
in_sellable_unit: z.any().optional(),
in_priceable_unit: z.any().optional()
})
.optional();
const fulfillmentIssueSchema = z.object({
cart_item_id: z.string().optional(),
issue_type: z.enum([
"FOUND_ITEM",
"PARTIAL_AVAILABILITY",
"OUT_OF_ITEM"
]),
action_type: z
.enum([
"REPLACE_FOR_ME",
"SUBSTITUTE_ME",
"REMOVE_ITEM",
"ALTERNATIVE_ITEM",
"SUBSTITUTION_REJECTED"
])
.optional(),
item_substitute: z.any().optional(),
item_availability: z
.object({
items_available: quantityObject
})
.optional()
});
const schema = z.object({
issues: z.array(fulfillmentIssueSchema).min(1)
});
const payload = schema.parse(req.body || {});
const data = await proxyService.resolveFulfillmentIssues({
orderId: req.params.orderId,
payload
});
return res.json({ success: true, data });
}
async function markOrderReady(req, res) {
const data = await proxyService.markOrderReady({
orderId: req.params.orderId,
payload: req.body || {}
});
return res.json({ success: true, data });
}
async function updateHours(req, res) { async function updateHours(req, res) {
const schema = z.object({ const schema = z.object({
merchantId: z.string().min(1), merchantId: z.string().min(1),
@ -235,6 +287,8 @@ module.exports = {
listOrders, listOrders,
getOrderById, getOrderById,
orderAction, orderAction,
resolveFulfillmentIssues,
markOrderReady,
updateHours, updateHours,
listProvisionableStores, listProvisionableStores,
listStores, listStores,

View File

@ -215,6 +215,30 @@ async function orderAction({ merchantId, orderId, action, payload }) {
}); });
} }
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 }) { async function updateStoreHours({ merchantId, storeId, payload }) {
const uberPath = interpolatePath(uberEndpoints.stores.updateHours, { storeId }); const uberPath = interpolatePath(uberEndpoints.stores.updateHours, { storeId });
return callUberApi({ return callUberApi({
@ -355,6 +379,8 @@ module.exports = {
ordersList, ordersList,
getOrderById, getOrderById,
orderAction, orderAction,
resolveFulfillmentIssues,
markOrderReady,
updateStoreHours, updateStoreHours,
listProvisionableStores, listProvisionableStores,
listStores, listStores,

View File

@ -101,6 +101,47 @@ router.get("/uber/orders", asyncHandler(controller.listOrders));
*/ */
router.get("/uber/orders/:orderId", asyncHandler(controller.getOrderById)); router.get("/uber/orders/:orderId", asyncHandler(controller.getOrderById));
/**
* @openapi
* /api/v1/uber/orders/{orderId}/fulfillment-issues:
* post:
* summary: Resolve retail fulfillment issues (found/partial/out-of-stock substitutions)
* tags:
* - Uber Orders
* parameters:
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Fulfillment issue update submitted
*/
router.post(
"/uber/orders/:orderId/fulfillment-issues",
asyncHandler(controller.resolveFulfillmentIssues)
);
/**
* @openapi
* /api/v1/uber/orders/{orderId}/ready:
* post:
* summary: Mark order ready for handoff/pickup
* tags:
* - Uber Orders
* parameters:
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Order marked ready
*/
router.post("/uber/orders/:orderId/ready", asyncHandler(controller.markOrderReady));
/** /**
* @openapi * @openapi
* /api/v1/uber/stores: * /api/v1/uber/stores: