feat: add typed retail fulfillment issue and order-ready endpoints
This commit is contained in:
parent
6b768bfbf5
commit
8eb11897a5
@ -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.
|
||||||
|
|||||||
38
docs/developer-portal/06-retail-fulfillment-audit.md
Normal file
38
docs/developer-portal/06-retail-fulfillment-audit.md
Normal 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
|
||||||
|
|
||||||
@ -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.
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user