Compare commits
No commits in common. "a89d67ebeedfaf08da1bcfc563f0c8a608b6651a" and "1ce9a388080e317b22ade48cdf787f10faf5a68a" have entirely different histories.
a89d67ebee
...
1ce9a38808
@ -14,7 +14,7 @@ This file intentionally separates high-priority vs extended APIs for easier team
|
|||||||
|
|
||||||
## Group B (Extended / Optional / Can Be Added Incrementally)
|
## Group B (Extended / Optional / Can Be Added Incrementally)
|
||||||
|
|
||||||
- Promotions (Promotions API v1.0.0 route set available)
|
- Promotions
|
||||||
- Ads / sponsored listings
|
- Ads / sponsored listings
|
||||||
- Payout and financial reconciliation (Reporting API route now available)
|
- Payout and financial reconciliation (Reporting API route now available)
|
||||||
- Store holiday/special schedules
|
- Store holiday/special schedules
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
# 05 Menu Example Payloads (v2)
|
|
||||||
|
|
||||||
These files are curated from the Uber v2 menu examples you shared, cleaned into valid JSON and ready for direct use in this wrapper.
|
|
||||||
|
|
||||||
Use with:
|
|
||||||
|
|
||||||
- Upload menu: `PUT /api/v1/uber/menu/replace`
|
|
||||||
- Update one item (sparse): `POST /api/v1/uber/menu/items`
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `docs/examples/menus/v2/empty-menu.json`
|
|
||||||
- Empty menu payload (can clear existing menu)
|
|
||||||
- `docs/examples/menus/v2/simple-menu.json`
|
|
||||||
- Simple menu with categories, items, and modifier groups
|
|
||||||
- `docs/examples/menus/v2/fulfillment-delivery-menu.json`
|
|
||||||
- Delivery-specific menu (`menu_type = MENU_TYPE_FULFILLMENT_DELIVERY`)
|
|
||||||
- `docs/examples/menus/v2/fulfillment-pickup-menu.json`
|
|
||||||
- Pickup-specific menu (`menu_type = MENU_TYPE_FULFILLMENT_PICK_UP`)
|
|
||||||
- `docs/examples/menus/v2/combo-bundled-items.json`
|
|
||||||
- Combo example with `bundled_items` + `core_price`
|
|
||||||
- `docs/examples/menus/v2/update-item-sparse.json`
|
|
||||||
- Sparse item update for `POST /v2/eats/stores/{store_id}/menus/items/{item_id}`
|
|
||||||
|
|
||||||
## Practical Notes
|
|
||||||
|
|
||||||
- For split fulfillment menus, upload `DELIVERY` first; after split, manage each menu type separately.
|
|
||||||
- Keep item IDs stable and avoid problematic characters like `/` and `;`.
|
|
||||||
- `alcoholic_items > 0` behaves as sticky in Uber and cannot be reverted by normal API update.
|
|
||||||
@ -6,27 +6,11 @@ Source checked: Uber Eats "Menu Integration" section shared by you.
|
|||||||
|
|
||||||
- Retrieve menu:
|
- Retrieve menu:
|
||||||
- `GET /api/v1/uber/menu`
|
- `GET /api/v1/uber/menu`
|
||||||
- aligned to upstream `GET /v2/eats/stores/{store_id}/menus`
|
|
||||||
- supports `menu_type` query values for delivery/pick-up/dine-in
|
|
||||||
- applies `Accept-Encoding: gzip` for large payload responses
|
|
||||||
- Full menu upload/replace:
|
- Full menu upload/replace:
|
||||||
- `PUT /api/v1/uber/menu/replace` (primary)
|
- `PUT /api/v1/uber/menu/replace` (primary)
|
||||||
- aligned to upstream `PUT /v2/eats/stores/{store_id}/menus`
|
|
||||||
- wrapper uploads gzip-compressed JSON (`Content-Encoding: gzip`)
|
|
||||||
- supports optional `menu.menu_type` (delivery/pick-up/dine-in)
|
|
||||||
- validates known menu_type enum values
|
|
||||||
- Individual item updates:
|
- Individual item updates:
|
||||||
- `POST /api/v1/uber/menu/items`
|
- `POST /api/v1/uber/menu/items`
|
||||||
- aligned to upstream `POST /v2/eats/stores/{store_id}/menus/items/{item_id}`
|
- supports stock/price style item-level updates via Menu Items endpoint
|
||||||
- request body is sparse update (only provided fields are changed)
|
|
||||||
- supports `menu_type` enum for split delivery/pickup/dine-in menus
|
|
||||||
- Example payload pack:
|
|
||||||
- Added curated v2 JSON examples under `docs/examples/menus/v2/`
|
|
||||||
- Added index doc `docs/developer-portal/05-menu-example-payloads.md`
|
|
||||||
- Added troubleshooting-aligned validation layer:
|
|
||||||
- no menus / no hours / short hours / overlapping visibility checks on upload
|
|
||||||
- UUID guard for `storeId` on menu routes
|
|
||||||
- `core_price >= price` guard on item updates
|
|
||||||
|
|
||||||
## Existing Before
|
## Existing Before
|
||||||
|
|
||||||
@ -38,3 +22,4 @@ Source checked: Uber Eats "Menu Integration" section shared by you.
|
|||||||
- Strict typed schemas for full menu payload entities (item, modifier group, category, menu)
|
- Strict typed schemas for full menu payload entities (item, modifier group, category, menu)
|
||||||
- Validation rules for image metadata limits and alcoholic item classifications
|
- Validation rules for image metadata limits and alcoholic item classifications
|
||||||
- Dedicated mapper helpers for `core_price` and `bundled_items` enrichment
|
- Dedicated mapper helpers for `core_price` and `bundled_items` enrichment
|
||||||
|
|
||||||
|
|||||||
@ -11,45 +11,11 @@ Menu sync between POS and Uber Eats:
|
|||||||
Current wrapper route for full replacement:
|
Current wrapper route for full replacement:
|
||||||
|
|
||||||
- `PUT /api/v1/uber/menu/replace`
|
- `PUT /api/v1/uber/menu/replace`
|
||||||
- upstream mapped to `PUT /v2/eats/stores/{store_id}/menus`
|
|
||||||
- request body is gzip-compressed for upload (`Content-Encoding: gzip`, `Content-Type: application/json`)
|
|
||||||
- optional `menu.menu_type` supported:
|
|
||||||
- `MENU_TYPE_FULFILLMENT_DELIVERY` (default)
|
|
||||||
- `MENU_TYPE_FULFILLMENT_PICK_UP`
|
|
||||||
- `MENU_TYPE_FULFILLMENT_DINE_IN`
|
|
||||||
|
|
||||||
Item update route:
|
Item update route:
|
||||||
|
|
||||||
- `POST /api/v1/uber/menu/items`
|
- `POST /api/v1/uber/menu/items`
|
||||||
- upstream mapped to `POST /v2/eats/stores/{store_id}/menus/items/{item_id}`
|
|
||||||
- sparse update only (only provided fields are changed)
|
|
||||||
- wrapper payload shape:
|
|
||||||
- `merchantId`
|
|
||||||
- `storeId`
|
|
||||||
- `itemId`
|
|
||||||
- `update` object with fields such as `price_info`, `suspension_info`, `menu_type`, `product_info`, `classifications`, `beverage_info`, `physical_properties_info`, `medication_info`, `nutritional_info`, `selling_info`
|
|
||||||
- reference catalogs for `product_info`:
|
|
||||||
- `GET /api/v1/uber/catalog/product-types`
|
|
||||||
- includes `product_types` and `mixin_types`
|
|
||||||
|
|
||||||
Menu fetch route:
|
|
||||||
|
|
||||||
- `GET /api/v1/uber/menu`
|
|
||||||
- upstream mapped to `GET /v2/eats/stores/{store_id}/menus`
|
|
||||||
- supports query `menu_type`:
|
|
||||||
- `MENU_TYPE_FULFILLMENT_DELIVERY` (default)
|
|
||||||
- `MENU_TYPE_FULFILLMENT_PICK_UP`
|
|
||||||
- `MENU_TYPE_FULFILLMENT_DINE_IN`
|
|
||||||
- sends `Accept-Encoding: gzip` upstream for large menu payloads
|
|
||||||
|
|
||||||
Best-practice note:
|
Best-practice note:
|
||||||
|
|
||||||
- Use API-managed menus only for integrated stores (avoid manual Menu Maker edits to prevent drift).
|
- Use API-managed menus only for integrated stores (avoid manual Menu Maker edits to prevent drift).
|
||||||
- Alcoholic item flag is effectively sticky in Uber (`alcoholic_items > 0` cannot be reverted by API update).
|
|
||||||
- Upload route performs preflight troubleshooting checks:
|
|
||||||
- no menus, no hours, short-hours (<60 min), overlapping item visibility intervals
|
|
||||||
|
|
||||||
Example payload pack:
|
|
||||||
|
|
||||||
- See [05-menu-example-payloads.md](./05-menu-example-payloads.md)
|
|
||||||
- JSON payloads are in `docs/examples/menus/v2/`
|
|
||||||
|
|||||||
@ -16,14 +16,6 @@ Source checked: Uber Eats "Webhook" section shared by you.
|
|||||||
- `resource_id`
|
- `resource_id`
|
||||||
- `resource_href`
|
- `resource_href`
|
||||||
- signature and dedupe key
|
- signature and dedupe key
|
||||||
- Added explicit event handling for `store.menu_refresh_request`:
|
|
||||||
- mapped via `store_id`
|
|
||||||
- records latest menu refresh request metadata on `uber_connections`
|
|
||||||
- stores webhook UUID and `X-Environment` marker
|
|
||||||
- Added explicit event handling for `eats.report.success`:
|
|
||||||
- maps by `job_id` / `workflow_id`
|
|
||||||
- marks report jobs completed
|
|
||||||
- persists report sections metadata for download orchestration
|
|
||||||
|
|
||||||
## Existing Before
|
## Existing Before
|
||||||
|
|
||||||
@ -35,3 +27,4 @@ Source checked: Uber Eats "Webhook" section shared by you.
|
|||||||
- Per-event downstream job workers (accept/deny SLA orchestration).
|
- Per-event downstream job workers (accept/deny SLA orchestration).
|
||||||
- Alerting if order accept/deny not sent before timeout window.
|
- Alerting if order accept/deny not sent before timeout window.
|
||||||
- Full typed schema validation per webhook `event_type`.
|
- Full typed schema validation per webhook `event_type`.
|
||||||
|
|
||||||
|
|||||||
@ -25,17 +25,7 @@ Common event types handled:
|
|||||||
- `delivery.state_changed`
|
- `delivery.state_changed`
|
||||||
- `store.provisioned`
|
- `store.provisioned`
|
||||||
- `store.deprovisioned`
|
- `store.deprovisioned`
|
||||||
- `store.menu_refresh_request`
|
|
||||||
- `store.status.changed`
|
- `store.status.changed`
|
||||||
- `eats.report.success`
|
|
||||||
|
|
||||||
Menu refresh handling:
|
|
||||||
|
|
||||||
- On `store.menu_refresh_request`, wrapper records the request on the mapped Uber store connection.
|
|
||||||
- Persisted fields include:
|
|
||||||
- `last_menu_refresh_requested_at`
|
|
||||||
- `last_menu_refresh_webhook_uuid`
|
|
||||||
- `last_webhook_environment` (from `X-Environment`)
|
|
||||||
|
|
||||||
Retail fulfillment follow-up:
|
Retail fulfillment follow-up:
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ Source checked: Uber Eats "Reporting Guide" section shared by you.
|
|||||||
|
|
||||||
- Dedicated Reporting route:
|
- Dedicated Reporting route:
|
||||||
- `POST /api/v1/uber/reporting/fetch`
|
- `POST /api/v1/uber/reporting/fetch`
|
||||||
- `POST /api/v1/uber/reporting/create`
|
|
||||||
- Uses `eats.report` client-credentials scope.
|
- Uses `eats.report` client-credentials scope.
|
||||||
- Retry policy implemented for safe transient failures only:
|
- Retry policy implemented for safe transient failures only:
|
||||||
- `429`, `408`, `500`, `502`, `503`, `504`, and network errors
|
- `429`, `408`, `500`, `502`, `503`, `504`, and network errors
|
||||||
@ -17,11 +16,6 @@ Source checked: Uber Eats "Reporting Guide" section shared by you.
|
|||||||
- ignore unknown extra columns
|
- ignore unknown extra columns
|
||||||
- tolerate missing columns with null/default behavior
|
- tolerate missing columns with null/default behavior
|
||||||
- report missing required headers in response metadata
|
- report missing required headers in response metadata
|
||||||
- Marketplace Reporting API async flow aligned:
|
|
||||||
- `POST /v1/eats/report` wrapper support with report-type/date constraints
|
|
||||||
- workflow tracking via `workflow_id`
|
|
||||||
- webhook completion handling for `eats.report.success`
|
|
||||||
- report sections metadata persisted on completion
|
|
||||||
|
|
||||||
## Existing Before
|
## Existing Before
|
||||||
|
|
||||||
@ -30,5 +24,7 @@ Source checked: Uber Eats "Reporting Guide" section shared by you.
|
|||||||
|
|
||||||
## Pending
|
## Pending
|
||||||
|
|
||||||
- Overnight polling scheduler/job orchestration for report section downloads
|
- Final typed endpoint wrappers for specific reporting reference endpoints once exact paths are shared
|
||||||
|
- Overnight polling scheduler/job orchestration
|
||||||
- Reconciliation materialization tables for settled vs provisional values
|
- Reconciliation materialization tables for settled vs provisional values
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ Reporting focus:
|
|||||||
Typed route:
|
Typed route:
|
||||||
|
|
||||||
- `POST /api/v1/uber/reporting/fetch`
|
- `POST /api/v1/uber/reporting/fetch`
|
||||||
- `POST /api/v1/uber/reporting/create`
|
|
||||||
|
|
||||||
Key behavior:
|
Key behavior:
|
||||||
|
|
||||||
@ -17,6 +16,4 @@ Key behavior:
|
|||||||
- Retries transient errors only (`408`, `429`, `500`, `502`, `503`, `504`, network timeouts)
|
- Retries transient errors only (`408`, `429`, `500`, `502`, `503`, `504`, network timeouts)
|
||||||
- Parses CSV by header names (not fixed column positions)
|
- Parses CSV by header names (not fixed column positions)
|
||||||
- Tolerates unknown columns and missing optional fields
|
- Tolerates unknown columns and missing optional fields
|
||||||
- Reporting job flow:
|
|
||||||
- create job returns `workflow_id`
|
|
||||||
- completion arrives via webhook `eats.report.success`
|
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# 18 Product Types Audit
|
|
||||||
|
|
||||||
Source checked: Uber Eats "Uber Product Types / Uber Mixin Types" section shared by you.
|
|
||||||
|
|
||||||
## Implemented Now
|
|
||||||
|
|
||||||
- Added a centralized in-code product catalog:
|
|
||||||
- [uberProductCatalog.js](../../src/config/uberProductCatalog.js)
|
|
||||||
- `PRODUCT_TYPES`
|
|
||||||
- `MIXIN_TYPES`
|
|
||||||
- Added a wrapper endpoint for developers/POS UIs:
|
|
||||||
- `GET /api/v1/uber/catalog/product-types`
|
|
||||||
- returns both product and mixin type lists as JSON
|
|
||||||
- Added Swagger route annotation for the catalog endpoint.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The catalog endpoint is reference data for building menu payloads (`product_info.product_type`, `product_info.product_traits`).
|
|
||||||
- Wrapper still keeps menu payload bodies flexible (`z.any`) to avoid blocking future Uber enum additions.
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
# 19 Menu Troubleshooting Audit
|
|
||||||
|
|
||||||
Source checked: Uber Eats "Troubleshooting Errors from the Menu API" section shared by you.
|
|
||||||
|
|
||||||
## Implemented Now
|
|
||||||
|
|
||||||
- Added proactive upload-menu payload validation before calling Uber:
|
|
||||||
- `No Menus Errors` guard:
|
|
||||||
- requires `menu.menus` to have at least one entry
|
|
||||||
- `No Hours Errors` guard:
|
|
||||||
- requires at least one valid `service_availability` interval
|
|
||||||
- `Short Hours Errors` guard:
|
|
||||||
- validates effective contiguous service windows are at least 60 minutes
|
|
||||||
- supports overnight-contiguous windows split across adjacent days
|
|
||||||
- `Invalid Visibility Errors` guard:
|
|
||||||
- detects overlapping `visibility_info.hours[].hours_of_week[].time_periods` for the same day
|
|
||||||
- Added stronger request path validation:
|
|
||||||
- menu routes now validate `storeId` as UUID
|
|
||||||
- helps prevent `invalid uuid` / `orgUUID must be a valid UUID` upstream errors
|
|
||||||
- Added update-item price sanity check:
|
|
||||||
- if both provided, `core_price` must be `>= price`
|
|
||||||
|
|
||||||
## Mapped to Wrapper
|
|
||||||
|
|
||||||
- `PUT /api/v1/uber/menu/replace`
|
|
||||||
- now runs menu troubleshooting validations before upstream call
|
|
||||||
- `POST /api/v1/uber/menu/items`
|
|
||||||
- validates `storeId` UUID and key `price_info` constraints
|
|
||||||
|
|
||||||
## Pending
|
|
||||||
|
|
||||||
- Live item existence precheck against current menu before sparse update (`nil item` prevention).
|
|
||||||
- Configurable per-market max price thresholds from Uber approval config.
|
|
||||||
- User-friendly remediation hints per failing field in structured error payloads.
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
# 20 Marketplace Reporting API 1.0.0 Audit
|
|
||||||
|
|
||||||
Source checked: Uber Eats "Marketplace Reporting API (1.0.0)" section shared by you.
|
|
||||||
|
|
||||||
## Implemented Now
|
|
||||||
|
|
||||||
- Added official create-report wrapper route:
|
|
||||||
- `POST /api/v1/uber/reporting/create`
|
|
||||||
- upstream `POST /v1/eats/report`
|
|
||||||
- returns `workflow_id`
|
|
||||||
- Added request validation:
|
|
||||||
- `report_type` enum validation (10 report types)
|
|
||||||
- requires at least one `store_uuids` or `group_uuids`
|
|
||||||
- validates date format by report type
|
|
||||||
- validates `start_date <= end_date`
|
|
||||||
- validates range/lookback constraints from the docs
|
|
||||||
- Added report job persistence (`report_jobs`):
|
|
||||||
- workflow tracking + report type + request windows + status
|
|
||||||
- Added webhook completion handling:
|
|
||||||
- `eats.report.success` marks job completed by `job_id`
|
|
||||||
- persists report sections metadata for downstream download workers
|
|
||||||
- Existing CSV fetch route retained:
|
|
||||||
- `POST /api/v1/uber/reporting/fetch` for direct CSV pulls where applicable
|
|
||||||
|
|
||||||
## Pending
|
|
||||||
|
|
||||||
- Worker queue for downloading and processing each `report_metadata.sections[]` URL.
|
|
||||||
- Retry orchestration for section URL fetches and long-running reconciliation pipelines.
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"menus": [
|
|
||||||
{
|
|
||||||
"id": "Menu",
|
|
||||||
"title": { "translations": { "en_us": "Menu" } },
|
|
||||||
"service_availability": [
|
|
||||||
{ "day_of_week": "monday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] }
|
|
||||||
],
|
|
||||||
"category_ids": ["Mains"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"id": "Mains",
|
|
||||||
"title": { "translations": { "en_us": "Mains" } },
|
|
||||||
"entities": [
|
|
||||||
{ "type": "ITEM", "id": "Burger_Combo" },
|
|
||||||
{ "type": "ITEM", "id": "Best_Fries" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "Burger_Combo",
|
|
||||||
"title": { "translations": { "en_us": "Best Burger Combo" } },
|
|
||||||
"price_info": { "price": 900, "overrides": [] },
|
|
||||||
"modifier_group_ids": { "ids": ["Select-Drink"] },
|
|
||||||
"bundled_items": [
|
|
||||||
{ "item_id": "Best_Fries", "core_price": 300, "included_quantity": 1 }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Best_Fries",
|
|
||||||
"title": { "translations": { "en_us": "Best Fries" } },
|
|
||||||
"price_info": { "price": 300, "overrides": [] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Root_Beer",
|
|
||||||
"title": { "translations": { "en_us": "Root Beer" } },
|
|
||||||
"price_info": { "price": 0, "core_price": 200, "overrides": [] }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modifier_groups": [
|
|
||||||
{
|
|
||||||
"id": "Select-Drink",
|
|
||||||
"title": { "translations": { "en_us": "Select Drink" } },
|
|
||||||
"quantity_info": { "quantity": { "max_permitted": 1 } },
|
|
||||||
"modifier_options": [{ "type": "ITEM", "id": "Root_Beer" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"display_options": {}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [],
|
|
||||||
"modifier_groups": [],
|
|
||||||
"categories": [],
|
|
||||||
"menus": [
|
|
||||||
{
|
|
||||||
"id": "empty_menu_id",
|
|
||||||
"title": { "translations": { "en_us": "Empty Menu" } },
|
|
||||||
"service_availability": [
|
|
||||||
{ "day_of_week": "monday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] },
|
|
||||||
{ "day_of_week": "tuesday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] },
|
|
||||||
{ "day_of_week": "wednesday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] },
|
|
||||||
{ "day_of_week": "thursday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] },
|
|
||||||
{ "day_of_week": "friday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] },
|
|
||||||
{ "day_of_week": "saturday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] },
|
|
||||||
{ "day_of_week": "sunday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] }
|
|
||||||
],
|
|
||||||
"category_ids": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"display_options": {}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"menu_type": "MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"menus": [
|
|
||||||
{
|
|
||||||
"id": "Special",
|
|
||||||
"title": { "translations": { "en_us": "Specials" } },
|
|
||||||
"service_availability": [
|
|
||||||
{ "day_of_week": "monday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] }
|
|
||||||
],
|
|
||||||
"category_ids": ["Specials"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"id": "Specials",
|
|
||||||
"title": { "translations": { "en_us": "Specials" } },
|
|
||||||
"entities": [
|
|
||||||
{ "type": "ITEM", "id": "Best_Burger_Delivery" },
|
|
||||||
{ "type": "ITEM", "id": "Best_Fries" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "Best_Burger_Delivery",
|
|
||||||
"title": { "translations": { "en_us": "Best Burger" } },
|
|
||||||
"price_info": { "price": 900 },
|
|
||||||
"tax_info": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Best_Fries",
|
|
||||||
"title": { "translations": { "en_us": "Best Fries" } },
|
|
||||||
"price_info": { "price": 300 },
|
|
||||||
"tax_info": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modifier_groups": [],
|
|
||||||
"display_options": {}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"menu_type": "MENU_TYPE_FULFILLMENT_PICK_UP",
|
|
||||||
"menus": [
|
|
||||||
{
|
|
||||||
"id": "Special",
|
|
||||||
"title": { "translations": { "en_us": "Specials" } },
|
|
||||||
"service_availability": [
|
|
||||||
{ "day_of_week": "monday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] }
|
|
||||||
],
|
|
||||||
"category_ids": ["Specials"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"id": "Specials",
|
|
||||||
"title": { "translations": { "en_us": "Specials" } },
|
|
||||||
"entities": [
|
|
||||||
{ "type": "ITEM", "id": "Best_Burger_Pickup" },
|
|
||||||
{ "type": "ITEM", "id": "Best_Fries" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "Best_Burger_Pickup",
|
|
||||||
"title": { "translations": { "en_us": "Best Burger + Pop" } },
|
|
||||||
"price_info": { "price": 1100 },
|
|
||||||
"tax_info": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Best_Fries",
|
|
||||||
"title": { "translations": { "en_us": "Best Fries" } },
|
|
||||||
"price_info": { "price": 500 },
|
|
||||||
"tax_info": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modifier_groups": [],
|
|
||||||
"display_options": {}
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "Coffee",
|
|
||||||
"title": { "translations": { "en_us": "Coffee" } },
|
|
||||||
"description": { "translations": { "en_us": "Deliciously roasted beans" } },
|
|
||||||
"modifier_group_ids": { "ids": ["Add-milk", "Add-sugar"] },
|
|
||||||
"price_info": { "price": 300 },
|
|
||||||
"tax_info": { "tax_rate": 8 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Milk",
|
|
||||||
"title": { "translations": { "en_us": "Milk" } },
|
|
||||||
"quantity_info": {
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"context_type": "MODIFIER_GROUP",
|
|
||||||
"context_value": "Add-milk",
|
|
||||||
"quantity": { "max_permitted": 1 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"price_info": {
|
|
||||||
"price": 0,
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"context_type": "MODIFIER_GROUP",
|
|
||||||
"context_value": "Add-milk",
|
|
||||||
"price": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"tax_info": { "tax_rate": 8 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Sugar",
|
|
||||||
"title": { "translations": { "en_us": "Sugar" } },
|
|
||||||
"quantity_info": {
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"context_type": "MODIFIER_GROUP",
|
|
||||||
"context_value": "Add-sugar",
|
|
||||||
"quantity": { "max_permitted": 2 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"price_info": {
|
|
||||||
"price": 2,
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"context_type": "MODIFIER_GROUP",
|
|
||||||
"context_value": "Add-sugar",
|
|
||||||
"price": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"tax_info": { "tax_rate": 8 }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modifier_groups": [
|
|
||||||
{
|
|
||||||
"id": "Add-milk",
|
|
||||||
"title": { "translations": { "en_us": "Add milk" } },
|
|
||||||
"quantity_info": { "quantity": { "max_permitted": 1 } },
|
|
||||||
"modifier_options": [{ "type": "ITEM", "id": "Milk" }]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "Add-sugar",
|
|
||||||
"title": { "translations": { "en_us": "Add sugar" } },
|
|
||||||
"quantity_info": { "quantity": { "max_permitted": 2 } },
|
|
||||||
"modifier_options": [{ "type": "ITEM", "id": "Sugar" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"id": "Drinks",
|
|
||||||
"title": { "translations": { "en_us": "Drinks" } },
|
|
||||||
"entities": [{ "type": "ITEM", "id": "Coffee" }]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"menus": [
|
|
||||||
{
|
|
||||||
"id": "All-day",
|
|
||||||
"title": { "translations": { "en_us": "All day" } },
|
|
||||||
"service_availability": [
|
|
||||||
{ "day_of_week": "monday", "time_periods": [{ "start_time": "00:00", "end_time": "23:59" }] }
|
|
||||||
],
|
|
||||||
"category_ids": ["Drinks"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"display_options": { "disable_item_instructions": true }
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"menu_type": "MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"price_info": {
|
|
||||||
"price": 1300,
|
|
||||||
"container_deposit": 100,
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"suspension_info": {
|
|
||||||
"suspension": null,
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"context_type": "MODIFIER_GROUP",
|
|
||||||
"context_value": "size",
|
|
||||||
"suspension": {
|
|
||||||
"suspend_until": 8640000000,
|
|
||||||
"reason": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"product_info": {
|
|
||||||
"target_market": "EU",
|
|
||||||
"gtin": "1354435445"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -290,19 +290,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/uber/catalog/product-types": {
|
|
||||||
"get": {
|
|
||||||
"summary": "Return Uber Product Types and Uber Mixin Types catalog",
|
|
||||||
"tags": [
|
|
||||||
"Uber Menu"
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Catalog returned"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/uber/menu/upsert": {
|
"/api/v1/uber/menu/upsert": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Legacy upsert helper for store menu",
|
"summary": "Legacy upsert helper for store menu",
|
||||||
@ -318,26 +305,26 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/uber/menu/replace": {
|
"/api/v1/uber/menu/replace": {
|
||||||
"put": {
|
"put": {
|
||||||
"summary": "Upload/replace store menu (PUT /v2/eats/stores/{store_id}/menus)",
|
"summary": "Replace store menu (full upload)",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Uber Menu"
|
"Uber Menu"
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Menu replaced (Uber returns 204 No Content)"
|
"description": "Menu replaced"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/uber/menu/items": {
|
"/api/v1/uber/menu/items": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Update single menu item (POST /v2/eats/stores/{store_id}/menus/items/{item_id})",
|
"summary": "Update individual menu items (stock/price updates)",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Uber Menu"
|
"Uber Menu"
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Menu item sparsely updated (Uber returns 204 No Content)"
|
"description": "Menu items updated"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -348,38 +335,6 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Uber Menu"
|
"Uber Menu"
|
||||||
],
|
],
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "merchantId",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "storeId",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "menu_type",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"MENU_TYPE_FULFILLMENT_PICK_UP",
|
|
||||||
"MENU_TYPE_FULFILLMENT_DINE_IN"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Defaults to MENU_TYPE_FULFILLMENT_DELIVERY."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Menu fetched"
|
"description": "Menu fetched"
|
||||||
@ -1071,96 +1026,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/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": {
|
"/api/v1/uber/reporting/fetch": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Fetch Uber reporting CSV with retries and header-based parsing",
|
"summary": "Fetch Uber reporting CSV with retries and header-based parsing",
|
||||||
@ -1174,19 +1039,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/uber/reporting/create": {
|
|
||||||
"post": {
|
|
||||||
"summary": "Create Marketplace Reporting API job (POST /v1/eats/report)",
|
|
||||||
"tags": [
|
|
||||||
"Uber Reporting"
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Report creation requested successfully (workflow_id returned)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/webhooks/uber": {
|
"/api/v1/webhooks/uber": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Ingest Uber webhook events",
|
"summary": "Ingest Uber webhook events",
|
||||||
|
|||||||
@ -167,32 +167,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Get Uber Product Types Catalog",
|
"name": "Replace Menu (PUT)",
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "x-api-key",
|
|
||||||
"value": "{{apiKey}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/catalog/product-types",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"uber",
|
|
||||||
"catalog",
|
|
||||||
"product-types"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Upload Menu (PUT v2)",
|
|
||||||
"request": {
|
"request": {
|
||||||
"method": "PUT",
|
"method": "PUT",
|
||||||
"header": [
|
"header": [
|
||||||
@ -207,7 +182,7 @@
|
|||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"menu_type\": \"MENU_TYPE_FULFILLMENT_DELIVERY\",\n \"menus\": [],\n \"categories\": [],\n \"items\": [],\n \"modifier_groups\": []\n }\n}"
|
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"categories\": []\n }\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/menu/replace",
|
"raw": "{{baseUrl}}/api/v1/uber/menu/replace",
|
||||||
@ -225,106 +200,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Upload Simple Menu Example",
|
"name": "Update Menu Items",
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "x-api-key",
|
|
||||||
"value": "{{apiKey}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"items\": [\n {\n \"id\": \"Coffee\",\n \"title\": { \"translations\": { \"en_us\": \"Coffee\" } },\n \"price_info\": { \"price\": 300 },\n \"tax_info\": { \"tax_rate\": 8 }\n }\n ],\n \"modifier_groups\": [],\n \"categories\": [\n {\n \"id\": \"Drinks\",\n \"title\": { \"translations\": { \"en_us\": \"Drinks\" } },\n \"entities\": [{ \"type\": \"ITEM\", \"id\": \"Coffee\" }]\n }\n ],\n \"menus\": [\n {\n \"id\": \"All-day\",\n \"title\": { \"translations\": { \"en_us\": \"All day\" } },\n \"service_availability\": [\n { \"day_of_week\": \"monday\", \"time_periods\": [{ \"start_time\": \"00:00\", \"end_time\": \"23:59\" }] }\n ],\n \"category_ids\": [\"Drinks\"]\n }\n ],\n \"display_options\": {}\n }\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/menu/replace",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"uber",
|
|
||||||
"menu",
|
|
||||||
"replace"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Upload Empty Menu Example",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "x-api-key",
|
|
||||||
"value": "{{apiKey}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"items\": [],\n \"modifier_groups\": [],\n \"categories\": [],\n \"menus\": [\n {\n \"id\": \"empty_menu_id\",\n \"title\": { \"translations\": { \"en_us\": \"Empty Menu\" } },\n \"service_availability\": [\n { \"day_of_week\": \"monday\", \"time_periods\": [{ \"start_time\": \"00:00\", \"end_time\": \"23:59\" }] }\n ],\n \"category_ids\": []\n }\n ],\n \"display_options\": {}\n }\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/menu/replace",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"uber",
|
|
||||||
"menu",
|
|
||||||
"replace"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Upload Menu - Short Hours Validation (Expected 400)",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "x-api-key",
|
|
||||||
"value": "{{apiKey}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"menu\": {\n \"items\": [],\n \"modifier_groups\": [],\n \"categories\": [],\n \"menus\": [\n {\n \"id\": \"short-hours-menu\",\n \"title\": { \"translations\": { \"en_us\": \"Short Hours\" } },\n \"service_availability\": [\n {\n \"day_of_week\": \"monday\",\n \"time_periods\": [\n { \"start_time\": \"00:00\", \"end_time\": \"00:00\" }\n ]\n }\n ],\n \"category_ids\": []\n }\n ]\n }\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/menu/replace",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"uber",
|
|
||||||
"menu",
|
|
||||||
"replace"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update Item (v2 Sparse)",
|
|
||||||
"request": {
|
"request": {
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"header": [
|
"header": [
|
||||||
@ -339,7 +215,7 @@
|
|||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"itemId\": \"item_1\",\n \"update\": {\n \"menu_type\": \"MENU_TYPE_FULFILLMENT_DELIVERY\",\n \"price_info\": {\n \"price\": 799\n },\n \"suspension_info\": {\n \"suspension\": null\n }\n }\n}"
|
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"storeId\": \"{{storeId}}\",\n \"items\": [\n {\n \"id\": \"item_1\",\n \"price_info\": {\n \"price\": 799\n },\n \"suspension_info\": {\n \"suspend_until\": null\n }\n }\n ]\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/menu/items",
|
"raw": "{{baseUrl}}/api/v1/uber/menu/items",
|
||||||
@ -356,44 +232,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Get Menu (v2)",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "x-api-key",
|
|
||||||
"value": "{{apiKey}}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/menu?merchantId={{merchantId}}&storeId={{storeId}}&menu_type=MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"uber",
|
|
||||||
"menu"
|
|
||||||
],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "merchantId",
|
|
||||||
"value": "{{merchantId}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "storeId",
|
|
||||||
"value": "{{storeId}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "menu_type",
|
|
||||||
"value": "MENU_TYPE_FULFILLMENT_DELIVERY"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "List Provisionable Stores",
|
"name": "List Provisionable Stores",
|
||||||
"request": {
|
"request": {
|
||||||
@ -1089,123 +927,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"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",
|
"name": "Get Store By ID",
|
||||||
"request": {
|
"request": {
|
||||||
@ -1531,39 +1252,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Create Marketplace Report Job",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "x-api-key",
|
|
||||||
"value": "{{apiKey}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"merchantId\": \"{{merchantId}}\",\n \"report_type\": \"ORDERS_AND_ITEMS_REPORT\",\n \"store_uuids\": [\"{{storeId}}\"],\n \"start_date\": \"2026-03-01\",\n \"end_date\": \"2026-03-10\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/uber/reporting/create",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"uber",
|
|
||||||
"reporting",
|
|
||||||
"create"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Set Holiday Hours",
|
"name": "Set Holiday Hours",
|
||||||
"request": {
|
"request": {
|
||||||
@ -1750,74 +1438,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Webhook Ingest - Menu Refresh (Simulation)",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "X-Uber-Signature",
|
|
||||||
"value": "replace-with-valid-hmac"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "X-Environment",
|
|
||||||
"value": "production"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"event_type\": \"store.menu_refresh_request\",\n \"partner_store_id\": \"123456\",\n \"resource_href\": \"https://api.uber.com/v1/eats/stores/{{storeId}}\",\n \"store_id\": \"{{storeId}}\",\n \"webhook_meta\": {\n \"client_id\": \"app_client_id\",\n \"webhook_config_id\": \"merchant-integration.menu-refresh-request\",\n \"webhook_msg_timestamp\": 1622813397,\n \"webhook_msg_uuid\": \"b2340f4c-6dd7-4d65-a9bc-016631a2a13a\"\n }\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/webhooks/uber",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"webhooks",
|
|
||||||
"uber"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Webhook Ingest - Report Success (Simulation)",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "X-Uber-Signature",
|
|
||||||
"value": "replace-with-valid-hmac"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"event_type\": \"eats.report.success\",\n \"event_id\": \"cd14f0bb-2d8c-44fb-9622-f6a4be18773e_2f7a1bdd-7993-4485-a11b-eacadce96b67\",\n \"job_id\": \"c7e05234-04ca-4460-8b03-d587df71228e\",\n \"report_type\": \"ORDERS_AND_ITEMS_REPORT\",\n \"start_time_ms\": 1742169600000,\n \"end_time_ms\": 1743119999000,\n \"report_metadata\": {\n \"sections\": []\n },\n \"webhook_meta\": {\n \"client_id\": \"ndkjscgfS5bvdiuyhv84sdhviudn\",\n \"webhook_config_id\": \"restaurant-financial-data.road-report-completion\",\n \"webhook_msg_timestamp\": 1743119999000,\n \"webhook_msg_uuid\": \"cd14f0bb-2d8c-44fb-9622-f6a4be18773e\"\n }\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/v1/webhooks/uber",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"v1",
|
|
||||||
"webhooks",
|
|
||||||
"uber"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Generic Uber Request",
|
"name": "Generic Uber Request",
|
||||||
"request": {
|
"request": {
|
||||||
@ -1871,10 +1491,6 @@
|
|||||||
{
|
{
|
||||||
"key": "orderId",
|
"key": "orderId",
|
||||||
"value": ""
|
"value": ""
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "promotionId",
|
|
||||||
"value": ""
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,13 +15,7 @@ const envSchema = z.object({
|
|||||||
WEBHOOK_BASIC_AUTH_USERNAME: z.string().optional(),
|
WEBHOOK_BASIC_AUTH_USERNAME: z.string().optional(),
|
||||||
WEBHOOK_BASIC_AUTH_PASSWORD: z.string().optional(),
|
WEBHOOK_BASIC_AUTH_PASSWORD: z.string().optional(),
|
||||||
SQLITE_PATH: z.string().default("./data/uber_wrapper.db"),
|
SQLITE_PATH: z.string().default("./data/uber_wrapper.db"),
|
||||||
WRAPPER_API_KEY: z.string().optional(),
|
WRAPPER_API_KEY: z.string().optional()
|
||||||
ASYNC_WORKERS_ENABLED: z
|
|
||||||
.preprocess((value) => String(value ?? "true").toLowerCase(), z.enum(["true", "false"]))
|
|
||||||
.transform((value) => value === "true"),
|
|
||||||
ASYNC_WORKER_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(1000),
|
|
||||||
ASYNC_WORKER_BATCH_SIZE: z.coerce.number().int().positive().default(5),
|
|
||||||
REPORT_DOWNLOAD_DIR: z.string().default("./data/reports")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = envSchema.safeParse(process.env);
|
const parsed = envSchema.safeParse(process.env);
|
||||||
@ -33,6 +27,5 @@ if (!parsed.success) {
|
|||||||
|
|
||||||
const env = parsed.data;
|
const env = parsed.data;
|
||||||
env.SQLITE_PATH = path.resolve(process.cwd(), env.SQLITE_PATH);
|
env.SQLITE_PATH = path.resolve(process.cwd(), env.SQLITE_PATH);
|
||||||
env.REPORT_DOWNLOAD_DIR = path.resolve(process.cwd(), env.REPORT_DOWNLOAD_DIR);
|
|
||||||
|
|
||||||
module.exports = env;
|
module.exports = env;
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
menu: {
|
menu: {
|
||||||
upsert: "/v1/eats/stores/{storeId}/menus",
|
upsert: "/v1/eats/stores/{storeId}/menus",
|
||||||
upload: "/v2/eats/stores/{storeId}/menus",
|
get: "/v1/eats/stores/{storeId}/menus",
|
||||||
get: "/v2/eats/stores/{storeId}/menus",
|
itemsUpdate: "/v1/eats/stores/{storeId}/menus/items"
|
||||||
itemUpdate: "/v2/eats/stores/{storeId}/menus/items/{itemId}"
|
|
||||||
},
|
},
|
||||||
orders: {
|
orders: {
|
||||||
list: "/v1/eats/stores/{storeId}/orders",
|
list: "/v1/eats/stores/{storeId}/orders",
|
||||||
@ -50,12 +49,6 @@ module.exports = {
|
|||||||
deliveryByoc: {
|
deliveryByoc: {
|
||||||
ingestCourierLocation: "/v1/eats/byoc/restaurants/orders/event/location"
|
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: {
|
webhooks: {
|
||||||
events: "/v1/eats/stores/{storeId}/event_feed"
|
events: "/v1/eats/stores/{storeId}/event_feed"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,346 +0,0 @@
|
|||||||
const RAW_PRODUCT_TYPES = `
|
|
||||||
ANIMALS_AND_PET_SUPPLIES
|
|
||||||
LIVE_ANIMALS
|
|
||||||
BIRD_SUPPLIES
|
|
||||||
CAT_SUPPLIES
|
|
||||||
DOG_SUPPLIES
|
|
||||||
FISH_SUPPLIES
|
|
||||||
REPTILE_AND_AMPHIBIAN_SUPPLIES
|
|
||||||
SMALL_ANIMAL_SUPPLIES
|
|
||||||
OTHER_PET_SUPPLIES
|
|
||||||
APPAREL_AND_ACCESSORIES
|
|
||||||
ARTS_AND_ENTERTAINMENT
|
|
||||||
ARTS_AND_CRAFTS
|
|
||||||
MUSICAL_INSTRUMENTS_AND_ACCESSORIES
|
|
||||||
PARTY_AND_OCCASION
|
|
||||||
BABY_AND_TODDLER
|
|
||||||
BABY_BATH_AND_BODY_CARE
|
|
||||||
BABY_DIAPERING_AND_WIPES
|
|
||||||
BABY_NURSING_AND_FEEDING
|
|
||||||
BABY_STROLLERS
|
|
||||||
BABY_CAR_SEATS
|
|
||||||
BABY_CARRIERS
|
|
||||||
BABY_TRANSPORT_ACCESSORIES
|
|
||||||
BABY_BEDDING_AND_DECOR
|
|
||||||
BABY_FURNITURE_AND_SAFETY_ACCESSORIES
|
|
||||||
BABY_CLOTHING_AND_ACCESSORIES
|
|
||||||
BABY_TOYS_AND_ACTIVITY_EQUIPMENT
|
|
||||||
BEVERAGE
|
|
||||||
BEVERAGE_ALCOHOLIC
|
|
||||||
BEVERAGE_ALCOHOLIC_BEER
|
|
||||||
BEVERAGE_ALCOHOLIC_WINE
|
|
||||||
LIQUORS_AND_SPIRITS
|
|
||||||
BEVERAGE_ALCOHOLIC_HARD_SELTZERS_AND_ALTERNATIVES
|
|
||||||
HARD_CIDER
|
|
||||||
SAKE
|
|
||||||
SHOCHU
|
|
||||||
BEVERAGE_NON-ALCOHOLIC
|
|
||||||
BEVERAGE_NON-ALCOHOLIC_SOFT_DRINKS
|
|
||||||
BEVERAGE_NON-ALCOHOLIC_COFFEE
|
|
||||||
BEVERAGE_NON-ALCOHOLIC_TEA
|
|
||||||
BEVERAGE_NON-ALCOHOLIC_WATER_AND_SELTZER
|
|
||||||
JUICE
|
|
||||||
POWDERED_DRINK_MIXES
|
|
||||||
SPORTS_ENERGY_AND_ELECTROLYTE_DRINKS
|
|
||||||
MILK_EGGNOG_AND_BUTTERMILK
|
|
||||||
MEAL_REPLACEMENTS_AND_PROTEIN_DRINKS
|
|
||||||
BEVERAGE_NON-ALCOHOLIC_WINE
|
|
||||||
BEVERAGE_NON-ALCOHOLIC_BEER
|
|
||||||
BEVERAGE_NON-ALCOHOLIC_SPIRITS
|
|
||||||
NON-ALCOHOLIC_COCKTAIL_MIXERS
|
|
||||||
BUSINESS_AND_INDUSTRIAL
|
|
||||||
CAMERAS_AND_OPTICS
|
|
||||||
CANNABIS
|
|
||||||
CANNABIS_VAPE
|
|
||||||
CANNABIS_EDIBLE
|
|
||||||
CANNABIS_BEVERAGE
|
|
||||||
CANNABIS_TOPICAL
|
|
||||||
CANNABIS_EXTRACT
|
|
||||||
CANNABIS_FLOWER
|
|
||||||
CANNABIS_PRE-ROLL
|
|
||||||
CANNABIS_OIL
|
|
||||||
CANNABIS_CAPSULE
|
|
||||||
CANNABIS_SPRAY
|
|
||||||
CANNABIS_OTHER
|
|
||||||
ELECTRONICS
|
|
||||||
FOOD
|
|
||||||
FOOD_RAW_MEATS_AND_FISH
|
|
||||||
PORK
|
|
||||||
BEEF
|
|
||||||
CHICKEN
|
|
||||||
TURKEY
|
|
||||||
OTHER_POULTRY
|
|
||||||
FISH_AND_SEAFOOD
|
|
||||||
LAMB
|
|
||||||
HOT_DOGS_SAUSAGES_AND_BACON
|
|
||||||
LUNCH_AND_DELI_MEAT
|
|
||||||
MEAT_AND_SEAFOOD_GIFTS
|
|
||||||
TOFU_SOY_AND_MEAT_ALTERNATIVES
|
|
||||||
MEAT_ALTERNATIVES
|
|
||||||
SEITAN
|
|
||||||
TEMPEH
|
|
||||||
TOFU
|
|
||||||
FOOD_PRODUCE
|
|
||||||
FRESH_FRUIT
|
|
||||||
FRESH_VEGETABLES
|
|
||||||
FRESH_HERBS
|
|
||||||
PRE-PACKAGED_FRUIT_AND_VEGETABLES
|
|
||||||
FOOD_BAKERY
|
|
||||||
BAGELS
|
|
||||||
BAKERY_ASSORTMENTS
|
|
||||||
BREADS_AND_BUNS
|
|
||||||
CAKES_AND_DESSERT_BARS
|
|
||||||
COFFEE_CAKES
|
|
||||||
CUPCAKES
|
|
||||||
DONUTS
|
|
||||||
ICE_CREAM_CONES
|
|
||||||
MUFFINS
|
|
||||||
PASTRIES_AND_SCONES
|
|
||||||
PIES_AND_TARTS
|
|
||||||
TACO_SHELLS_AND_TOSTADAS
|
|
||||||
TORTILLAS_AND_WRAPS
|
|
||||||
CANDIED_AND_CHOCOLATE_COVERED_FRUIT
|
|
||||||
CANDY_COOKIES_AND_CHOCOLATE
|
|
||||||
CANDY_AND_CHOCOLATE
|
|
||||||
COOKIES_AND_SWEET_BISCUITS
|
|
||||||
FOOD_SNACKS_AND_CANDY
|
|
||||||
APPLESAUCE_AND_FRUIT_CUPS
|
|
||||||
BREADSTICKS
|
|
||||||
BARS
|
|
||||||
CHEESE_PUFFS
|
|
||||||
CHIPS_AND_CRISPS
|
|
||||||
CRACKERS
|
|
||||||
CROUTONS
|
|
||||||
DRIED_FRUIT_AND_RAISINS
|
|
||||||
FRUIT_LEATHER
|
|
||||||
FRUIT_SNACKS
|
|
||||||
GUMN_AND_MINTS
|
|
||||||
ICE_CREAM_CONES_AND_TOPPINGS
|
|
||||||
JERKY_AND_PORK_RINDS
|
|
||||||
NUTS_AND_SEEDS
|
|
||||||
POPCORN
|
|
||||||
PRETZELS
|
|
||||||
PUDDING_AND_GELATIN_SNACKS
|
|
||||||
PUFFED_RICE_CAKES
|
|
||||||
SALAD_TOPPINGS
|
|
||||||
SESAME_STICKS
|
|
||||||
SNACK_CAKES
|
|
||||||
STICKY_RICE_CAKES
|
|
||||||
TRAIL_AND_SNACK_MIXES
|
|
||||||
FOOD_FROZEN_FOODS
|
|
||||||
FROZEN_APPETIZERS_AND_SNACKS
|
|
||||||
FROZEN_BREAD_AND_DOUGH
|
|
||||||
FROZEN_BREAKFAST_FOODS
|
|
||||||
FROZEN_DESSERTS_AND_TOPPINGS
|
|
||||||
FROZEN_FRUIT
|
|
||||||
ICE
|
|
||||||
ICE_CREAM_AND_NOVELTIES
|
|
||||||
FROZEN_JUICE
|
|
||||||
FROZEN_MEALS_AND_ENTREES
|
|
||||||
FROZEN_MEATS
|
|
||||||
FROZEN_PASTA_AND_SAUCES
|
|
||||||
FROZEN_PIZZA
|
|
||||||
FROZEN_POTATOES_AND_ONION_RINGS
|
|
||||||
FROZEN_FISH_AND_SEAFOOD
|
|
||||||
FROZEN_VEGETABLES
|
|
||||||
FOOD_DAIRY
|
|
||||||
BUTTER_AND_MARGARINE
|
|
||||||
CHEESE
|
|
||||||
COFFEE_CREAMER
|
|
||||||
COTTAGE_CHEESE
|
|
||||||
CREAM
|
|
||||||
EGGS
|
|
||||||
SOUR_CREAM
|
|
||||||
WHIPPED_CREAM
|
|
||||||
YOGURT
|
|
||||||
FOOD_CONDIMENTS
|
|
||||||
COCKTAIL_SAUCE
|
|
||||||
CURRY_SAUCE
|
|
||||||
DESSERT_TOPPINGS
|
|
||||||
FISH_SAUCE
|
|
||||||
GRAVY
|
|
||||||
HONEY
|
|
||||||
HORSERADISH_SAUCE
|
|
||||||
HOT_SAUCE
|
|
||||||
KETCHUP
|
|
||||||
MARINADES_AND_GRILLING_SAUCES
|
|
||||||
MAYONNAISE
|
|
||||||
MUSTARD
|
|
||||||
OLIVES_AND_CAPERS
|
|
||||||
PASTA_SAUCE
|
|
||||||
PICKLED_FRUITS_AND_VEGETABLES
|
|
||||||
PIZZA_SAUCE
|
|
||||||
RELISH_AND_CHUTNEY
|
|
||||||
SALAD_DRESSING
|
|
||||||
SATAY_SAUCE
|
|
||||||
SOY_SAUCE
|
|
||||||
SWEET_AND_SOUR_SAUCES
|
|
||||||
SYRUP
|
|
||||||
TAHINI
|
|
||||||
TARTAR_SAUCE
|
|
||||||
WHITE_AND_CREAM_SAUCES
|
|
||||||
WORCESTERSHIRE_SAUCE
|
|
||||||
COOKING_AND_BAKING_INGREDIENTS
|
|
||||||
BAKING_CHIPS
|
|
||||||
BAKING_CHOCOLATE
|
|
||||||
BAKING_FLAVORS_AND_EXTRACTS
|
|
||||||
BAKING_MIXERS
|
|
||||||
BAKING_POWDER
|
|
||||||
BAKING_SODA
|
|
||||||
BATTER_AND_COATING_MIXES
|
|
||||||
BEAN_PASTE
|
|
||||||
BREAD_CRUMBS
|
|
||||||
CANNED_AND_DRY_MILK
|
|
||||||
COOKIE_DECORATING_KITS
|
|
||||||
COOKING_OILS
|
|
||||||
COOKING_STARCH
|
|
||||||
COOKING_WINE
|
|
||||||
CORN_SYRUP
|
|
||||||
DOUGH
|
|
||||||
EDIBLE_BAKING_DECORATIONS
|
|
||||||
EGG_REPLACERS
|
|
||||||
FLOSS_SUGAR
|
|
||||||
FLOUR
|
|
||||||
FOOD_COLORING
|
|
||||||
FROSTING_AND_ICING
|
|
||||||
LEMON_AND_LIME_JUICE
|
|
||||||
MARSHMALLOWS
|
|
||||||
MEAL
|
|
||||||
MOLASSES
|
|
||||||
PIE_AND_PASTRY_FILLINGS
|
|
||||||
SHORTENING_AND_LARD
|
|
||||||
STARTER_CULTURES
|
|
||||||
SUGAR_AND_SWEETENERS
|
|
||||||
TAPIOCA_PEARLS
|
|
||||||
TOMATO_PASTE
|
|
||||||
UNFLAVORED_GELATIN
|
|
||||||
VINEGAR
|
|
||||||
WAFFLE_AND_PANCAKE_MIXES
|
|
||||||
YEAST
|
|
||||||
DIPS_AND_SPREADS
|
|
||||||
APPLE_BUTTER
|
|
||||||
CHEESE_DIPS_AND_SPREADS
|
|
||||||
CREAM_CHEESE
|
|
||||||
GUACAMOLE
|
|
||||||
HUMMUS
|
|
||||||
JAMS_AND_JELLIES
|
|
||||||
NUT_BUTTERS
|
|
||||||
SALSA
|
|
||||||
TAPENADE
|
|
||||||
VEGETABLE_DIP
|
|
||||||
GRAINS_RICE_AND_LEGUMES
|
|
||||||
AMARANTH
|
|
||||||
BARLEY
|
|
||||||
BUCKWHEAT
|
|
||||||
MILLET
|
|
||||||
QUINOA
|
|
||||||
RICE
|
|
||||||
RYE
|
|
||||||
WHEAT
|
|
||||||
DRIED_BEANS
|
|
||||||
DRIED_LENTILS_AND_PEAS
|
|
||||||
CEREAL_AND_GRANOLA
|
|
||||||
OATS_GRITS_AND_HOT_CEREAL
|
|
||||||
CEREAL_OATMEAL_AND_GRANOLA
|
|
||||||
FOOD_PREPARED_FOOD
|
|
||||||
INSTANT_MEALS
|
|
||||||
PASTA_AND_NOODLES
|
|
||||||
SEASONINGS_AND_SPICES
|
|
||||||
CANNED_PRODUCE_AND_SOUPS
|
|
||||||
CANNED_AND_PREPARED_BEANS_LENTILS_AND_PEAS
|
|
||||||
CANNED_AND_JARRED_FRUITS
|
|
||||||
CANNED_MEAT_AND_SEAFOOD
|
|
||||||
CANNED_AND_BOXED_SOUP_STOCKS_AND_BROTHS
|
|
||||||
FOOD_AND_NON-ALCOHOLIC_BEVERAGE_COMBO
|
|
||||||
FOOD_AND_ALCOHOLIC_BEVERAGE_COMBO
|
|
||||||
FURNITURE
|
|
||||||
HARDWARE
|
|
||||||
HEALTH_AND_BEAUTY
|
|
||||||
HEALTH_AND_BEAUTY_FAMILY_PLANNING
|
|
||||||
MATURE_CONDOMS
|
|
||||||
MATURE_LUBRICANTS
|
|
||||||
HEALTH_AND_BEAUTY_FIRST_AID
|
|
||||||
VITAMINS_AND_SUPPLEMENTS
|
|
||||||
MEDICAL_SUPPLIES_AND_MONITORS
|
|
||||||
HEALTH_AND_BEAUTY_MEDICINE_AND_DRUGS
|
|
||||||
HEALTH_AND_BEAUTY_MEDICINE_AND_DRUGS_ANTACIDS_AND_ANTIFLATULENTS
|
|
||||||
HEALTH_AND_BEAUTY_MEDICINE_AND_DRUGS_COUGH_SUPRESSANTS
|
|
||||||
HEALTH_AND_BEAUTY_MEDICINE_AND_DRUGS_COLD_AND_FLU_MEDICINES
|
|
||||||
HEALTH_AND_BEAUTY_MEDICINE_AND_DRUGS_PRESCRIPTION_DRUGS
|
|
||||||
BATH_AND_BODY
|
|
||||||
COTTON_SWABS_AND_BALLS
|
|
||||||
DEODORANT_AND_ANTIPERSPIRANT
|
|
||||||
EYE_AND_EAR_CARE
|
|
||||||
FEMININE_CARE
|
|
||||||
HAIR_CARE
|
|
||||||
HAND_AND_FOOT_CARE
|
|
||||||
JEWELRY_CLEANING_AND_CARE
|
|
||||||
MAKEUP_NAILS_AND_COSMETIC_TOOLS
|
|
||||||
ORAL_CARE
|
|
||||||
PERFUME_AND_COLOGNE
|
|
||||||
SHAVING_AND_GROOMING
|
|
||||||
SKINCARE
|
|
||||||
SLEEP_AND_MASSAGE
|
|
||||||
HOUSEHOLD_ESSENTIALS
|
|
||||||
HOUSEHOLD_CLEANING_SUPPLIES
|
|
||||||
HOUSEHOLD_PAPER_PRODUCTS
|
|
||||||
LAUNDRY_SUPPLIES
|
|
||||||
TRASH_BINS_AND_BAGS
|
|
||||||
FOOD_STORAGE_AND_ACCESSORIES
|
|
||||||
AIR_FRESHENERS_CANDLES_AND_FRAGRANCE
|
|
||||||
INSECT_AND_PEST_CONTROL
|
|
||||||
SHOE_CARE_AND_TOOLS
|
|
||||||
LIGHTERS_AND_MATCHES
|
|
||||||
HOME_AND_GARDEN
|
|
||||||
HOME_AND_GARDEN_BATHROOM_ACCESSORIES
|
|
||||||
HOME_AND_GARDEN_DECOR
|
|
||||||
ORGANIZERS
|
|
||||||
HOME_AND_GARDEN_KITCHEN_AND_DINING
|
|
||||||
HOME_AND_GARDEN_LAWN_AND_GARDEN
|
|
||||||
HOME_AND_GARDEN_LIGHTING_AND_LIGHTING_ACCESSORIES
|
|
||||||
HOME_AND_GARDEN_LINENS_AND_BEDDING
|
|
||||||
HOME_AND_GARDEN_POOL_AND_SPA
|
|
||||||
HOME_AND_GARDEN_SMOKING_ACCESSORIES
|
|
||||||
HOME_AND_GARDEN_GARDEN_CHEMICALS
|
|
||||||
HOME_AND_GARDEN_FUEL
|
|
||||||
FLOWERS_AND_PLANTS
|
|
||||||
FLOWERS_AND_BOUQUETS
|
|
||||||
HOME_AND_GARDEN_PLANTS
|
|
||||||
LUGGAGE_AND_BAGS
|
|
||||||
MATURE
|
|
||||||
MATURE_ADULT_GAMES
|
|
||||||
MATURE_VIBRATORS
|
|
||||||
LINGERIE
|
|
||||||
LOTTERY
|
|
||||||
MEDIA
|
|
||||||
MEDIA_BOOKS
|
|
||||||
MEDIA_DVD’S_AND_VIDEOS
|
|
||||||
MEDIA_MAGAZINES_AND_NEWSPAPERS
|
|
||||||
MEDIA_MUSIC_AND_SOUND_RECORDINGS
|
|
||||||
OFFICE_SUPPLIES
|
|
||||||
SOFTWARE
|
|
||||||
SPORTING_GOODS
|
|
||||||
TOBACCO
|
|
||||||
TOBACCO_CIGARETTES
|
|
||||||
TOBACCO_CIGARS
|
|
||||||
TOBACCO_E-CIGARETTE
|
|
||||||
TOBACCO_VAPES
|
|
||||||
TOBACCO_SMOKELESS_TOBACCO
|
|
||||||
TOYS_AND_GAMES
|
|
||||||
VEHICLES_AND_PARTS
|
|
||||||
`;
|
|
||||||
|
|
||||||
const PRODUCT_TYPES = Array.from(
|
|
||||||
new Set(
|
|
||||||
RAW_PRODUCT_TYPES.split(/\r?\n/)
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const MIXIN_TYPES = ["CONTAINS_ALCOHOL", "CONTAINS_CANNABIS"];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
PRODUCT_TYPES,
|
|
||||||
MIXIN_TYPES
|
|
||||||
};
|
|
||||||
@ -11,8 +11,5 @@ module.exports = {
|
|||||||
webhookRepository: repositories.webhookRepository,
|
webhookRepository: repositories.webhookRepository,
|
||||||
apiLogRepository: repositories.apiLogRepository,
|
apiLogRepository: repositories.apiLogRepository,
|
||||||
appTokenRepository: repositories.appTokenRepository,
|
appTokenRepository: repositories.appTokenRepository,
|
||||||
tokenRequestLogRepository: repositories.tokenRequestLogRepository,
|
tokenRequestLogRepository: repositories.tokenRequestLogRepository
|
||||||
reportJobRepository: repositories.reportJobRepository,
|
|
||||||
asyncJobRepository: repositories.asyncJobRepository,
|
|
||||||
reportSectionRepository: repositories.reportSectionRepository
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -107,31 +107,6 @@ const uberConnectionRepository = {
|
|||||||
return this.findByMerchantId(merchantId);
|
return this.findByMerchantId(merchantId);
|
||||||
},
|
},
|
||||||
|
|
||||||
markMenuRefreshRequestedByStoreId(uberStoreId, payload) {
|
|
||||||
const existing = this.findByUberStoreId(uberStoreId);
|
|
||||||
if (!existing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const timestamp = nowIso();
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE uber_connections
|
|
||||||
SET last_menu_refresh_requested_at = ?,
|
|
||||||
last_menu_refresh_webhook_uuid = ?,
|
|
||||||
last_webhook_environment = ?,
|
|
||||||
updated_at = ?
|
|
||||||
WHERE uber_store_id = ?
|
|
||||||
`
|
|
||||||
).run(
|
|
||||||
payload?.requestedAt || timestamp,
|
|
||||||
payload?.webhookMsgUuid || null,
|
|
||||||
payload?.environment || null,
|
|
||||||
timestamp,
|
|
||||||
uberStoreId
|
|
||||||
);
|
|
||||||
return this.findByUberStoreId(uberStoreId);
|
|
||||||
},
|
|
||||||
|
|
||||||
list() {
|
list() {
|
||||||
return db
|
return db
|
||||||
.prepare(`
|
.prepare(`
|
||||||
@ -145,10 +120,6 @@ const uberConnectionRepository = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const webhookRepository = {
|
const webhookRepository = {
|
||||||
findById(id) {
|
|
||||||
return db.prepare("SELECT * FROM webhook_events WHERE id = ? LIMIT 1").get(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
findByDedupeKey(dedupeKey) {
|
findByDedupeKey(dedupeKey) {
|
||||||
if (!dedupeKey) {
|
if (!dedupeKey) {
|
||||||
return null;
|
return null;
|
||||||
@ -195,31 +166,6 @@ const webhookRepository = {
|
|||||||
`);
|
`);
|
||||||
stmt.run(row);
|
stmt.run(row);
|
||||||
return row;
|
return row;
|
||||||
},
|
|
||||||
|
|
||||||
markProcessed(id) {
|
|
||||||
const timestamp = nowIso();
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE webhook_events
|
|
||||||
SET processing_status = 'processed',
|
|
||||||
processed_at = ?,
|
|
||||||
last_error = NULL
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
).run(timestamp, id);
|
|
||||||
},
|
|
||||||
|
|
||||||
markFailed(id, errorMessage) {
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE webhook_events
|
|
||||||
SET processing_status = 'failed',
|
|
||||||
processed_at = ?,
|
|
||||||
last_error = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
).run(nowIso(), String(errorMessage || "Unknown webhook processing error"), id);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -456,15 +402,6 @@ const appTokenRepository = {
|
|||||||
`
|
`
|
||||||
)
|
)
|
||||||
.get(provider, grantType, scope);
|
.get(provider, grantType, scope);
|
||||||
},
|
|
||||||
|
|
||||||
deleteByProviderGrantScope({ provider, grantType, scope }) {
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
DELETE FROM app_tokens
|
|
||||||
WHERE provider = ? AND grant_type = ? AND scope = ?
|
|
||||||
`
|
|
||||||
).run(provider, grantType, scope);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -497,355 +434,11 @@ const tokenRequestLogRepository = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const reportJobRepository = {
|
|
||||||
createRequested({
|
|
||||||
merchantId,
|
|
||||||
workflowId,
|
|
||||||
reportType,
|
|
||||||
storeUuids,
|
|
||||||
groupUuids,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
}) {
|
|
||||||
const existing = this.findByWorkflowId(workflowId);
|
|
||||||
const timestamp = nowIso();
|
|
||||||
const row = {
|
|
||||||
id: existing?.id || uuidv4(),
|
|
||||||
merchant_id: merchantId || existing?.merchant_id || null,
|
|
||||||
workflow_id: workflowId,
|
|
||||||
report_type: reportType,
|
|
||||||
store_uuids_json: JSON.stringify(storeUuids || []),
|
|
||||||
group_uuids_json: JSON.stringify(groupUuids || []),
|
|
||||||
start_date: startDate,
|
|
||||||
end_date: endDate,
|
|
||||||
status: existing?.status === "completed" ? "completed" : "requested",
|
|
||||||
webhook_event_id: existing?.webhook_event_id || null,
|
|
||||||
webhook_msg_uuid: existing?.webhook_msg_uuid || null,
|
|
||||||
report_sections_json: existing?.report_sections_json || null,
|
|
||||||
webhook_payload_json: existing?.webhook_payload_json || null,
|
|
||||||
created_at: existing?.created_at || timestamp,
|
|
||||||
completed_at: existing?.completed_at || null,
|
|
||||||
updated_at: timestamp
|
|
||||||
};
|
|
||||||
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
INSERT INTO report_jobs (
|
|
||||||
id, merchant_id, workflow_id, report_type, store_uuids_json, group_uuids_json,
|
|
||||||
start_date, end_date, status, webhook_event_id, webhook_msg_uuid, report_sections_json,
|
|
||||||
webhook_payload_json, created_at, completed_at, updated_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
@id, @merchant_id, @workflow_id, @report_type, @store_uuids_json, @group_uuids_json,
|
|
||||||
@start_date, @end_date, @status, @webhook_event_id, @webhook_msg_uuid, @report_sections_json,
|
|
||||||
@webhook_payload_json, @created_at, @completed_at, @updated_at
|
|
||||||
)
|
|
||||||
ON CONFLICT(workflow_id) DO UPDATE SET
|
|
||||||
merchant_id = excluded.merchant_id,
|
|
||||||
report_type = excluded.report_type,
|
|
||||||
store_uuids_json = excluded.store_uuids_json,
|
|
||||||
group_uuids_json = excluded.group_uuids_json,
|
|
||||||
start_date = excluded.start_date,
|
|
||||||
end_date = excluded.end_date,
|
|
||||||
updated_at = excluded.updated_at
|
|
||||||
`
|
|
||||||
).run(row);
|
|
||||||
|
|
||||||
return this.findByWorkflowId(workflowId);
|
|
||||||
},
|
|
||||||
|
|
||||||
markSuccessFromWebhook({
|
|
||||||
workflowId,
|
|
||||||
eventId,
|
|
||||||
webhookMsgUuid,
|
|
||||||
reportType,
|
|
||||||
sections,
|
|
||||||
payload
|
|
||||||
}) {
|
|
||||||
const existing = this.findByWorkflowId(workflowId);
|
|
||||||
const timestamp = nowIso();
|
|
||||||
|
|
||||||
if (!existing) {
|
|
||||||
const row = {
|
|
||||||
id: uuidv4(),
|
|
||||||
merchant_id: null,
|
|
||||||
workflow_id: workflowId,
|
|
||||||
report_type: reportType || "UNKNOWN",
|
|
||||||
store_uuids_json: JSON.stringify([]),
|
|
||||||
group_uuids_json: JSON.stringify([]),
|
|
||||||
start_date: "",
|
|
||||||
end_date: "",
|
|
||||||
status: "completed",
|
|
||||||
webhook_event_id: eventId || null,
|
|
||||||
webhook_msg_uuid: webhookMsgUuid || null,
|
|
||||||
report_sections_json: JSON.stringify(sections || []),
|
|
||||||
webhook_payload_json: JSON.stringify(payload || {}),
|
|
||||||
created_at: timestamp,
|
|
||||||
completed_at: timestamp,
|
|
||||||
updated_at: timestamp
|
|
||||||
};
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
INSERT INTO report_jobs (
|
|
||||||
id, merchant_id, workflow_id, report_type, store_uuids_json, group_uuids_json,
|
|
||||||
start_date, end_date, status, webhook_event_id, webhook_msg_uuid, report_sections_json,
|
|
||||||
webhook_payload_json, created_at, completed_at, updated_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
@id, @merchant_id, @workflow_id, @report_type, @store_uuids_json, @group_uuids_json,
|
|
||||||
@start_date, @end_date, @status, @webhook_event_id, @webhook_msg_uuid, @report_sections_json,
|
|
||||||
@webhook_payload_json, @created_at, @completed_at, @updated_at
|
|
||||||
)
|
|
||||||
`
|
|
||||||
).run(row);
|
|
||||||
return this.findByWorkflowId(workflowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE report_jobs
|
|
||||||
SET status = 'completed',
|
|
||||||
report_type = COALESCE(?, report_type),
|
|
||||||
webhook_event_id = ?,
|
|
||||||
webhook_msg_uuid = ?,
|
|
||||||
report_sections_json = ?,
|
|
||||||
webhook_payload_json = ?,
|
|
||||||
completed_at = ?,
|
|
||||||
updated_at = ?
|
|
||||||
WHERE workflow_id = ?
|
|
||||||
`
|
|
||||||
).run(
|
|
||||||
reportType || null,
|
|
||||||
eventId || null,
|
|
||||||
webhookMsgUuid || null,
|
|
||||||
JSON.stringify(sections || []),
|
|
||||||
JSON.stringify(payload || {}),
|
|
||||||
timestamp,
|
|
||||||
timestamp,
|
|
||||||
workflowId
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.findByWorkflowId(workflowId);
|
|
||||||
},
|
|
||||||
|
|
||||||
findByWorkflowId(workflowId) {
|
|
||||||
return db.prepare("SELECT * FROM report_jobs WHERE workflow_id = ? LIMIT 1").get(workflowId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const asyncJobRepository = {
|
|
||||||
enqueue({
|
|
||||||
jobType,
|
|
||||||
payload,
|
|
||||||
maxAttempts = 7,
|
|
||||||
nextRunAt = nowIso()
|
|
||||||
}) {
|
|
||||||
const timestamp = nowIso();
|
|
||||||
const row = {
|
|
||||||
id: uuidv4(),
|
|
||||||
job_type: jobType,
|
|
||||||
payload_json: JSON.stringify(payload || {}),
|
|
||||||
status: "queued",
|
|
||||||
attempt_count: 0,
|
|
||||||
max_attempts: maxAttempts,
|
|
||||||
next_run_at: nextRunAt,
|
|
||||||
last_error: null,
|
|
||||||
lock_token: null,
|
|
||||||
locked_at: null,
|
|
||||||
created_at: timestamp,
|
|
||||||
updated_at: timestamp
|
|
||||||
};
|
|
||||||
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
INSERT INTO async_jobs (
|
|
||||||
id, job_type, payload_json, status, attempt_count, max_attempts, next_run_at,
|
|
||||||
last_error, lock_token, locked_at, created_at, updated_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
@id, @job_type, @payload_json, @status, @attempt_count, @max_attempts, @next_run_at,
|
|
||||||
@last_error, @lock_token, @locked_at, @created_at, @updated_at
|
|
||||||
)
|
|
||||||
`
|
|
||||||
).run(row);
|
|
||||||
|
|
||||||
return row;
|
|
||||||
},
|
|
||||||
|
|
||||||
claimNext({ workerId, lockTimeoutSeconds = 120 }) {
|
|
||||||
const timestamp = nowIso();
|
|
||||||
const lockCutoffIso = new Date(Date.now() - lockTimeoutSeconds * 1000).toISOString();
|
|
||||||
const selectStmt = db.prepare(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM async_jobs
|
|
||||||
WHERE (status = 'queued' OR status = 'retry')
|
|
||||||
AND next_run_at <= ?
|
|
||||||
AND (locked_at IS NULL OR locked_at < ?)
|
|
||||||
ORDER BY created_at ASC
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
);
|
|
||||||
const updateStmt = db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE async_jobs
|
|
||||||
SET status = 'processing',
|
|
||||||
lock_token = ?,
|
|
||||||
locked_at = ?,
|
|
||||||
attempt_count = attempt_count + 1,
|
|
||||||
updated_at = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
const tx = db.transaction(() => {
|
|
||||||
const row = selectStmt.get(timestamp, lockCutoffIso);
|
|
||||||
if (!row) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
updateStmt.run(workerId, timestamp, timestamp, row.id);
|
|
||||||
return db.prepare("SELECT * FROM async_jobs WHERE id = ? LIMIT 1").get(row.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return tx();
|
|
||||||
},
|
|
||||||
|
|
||||||
markCompleted(id) {
|
|
||||||
const timestamp = nowIso();
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE async_jobs
|
|
||||||
SET status = 'completed',
|
|
||||||
lock_token = NULL,
|
|
||||||
locked_at = NULL,
|
|
||||||
updated_at = ?,
|
|
||||||
last_error = NULL
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
).run(timestamp, id);
|
|
||||||
},
|
|
||||||
|
|
||||||
markFailedOrRetry({ id, attemptCount, maxAttempts, errorMessage }) {
|
|
||||||
const timestamp = nowIso();
|
|
||||||
if (attemptCount >= maxAttempts) {
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE async_jobs
|
|
||||||
SET status = 'failed',
|
|
||||||
lock_token = NULL,
|
|
||||||
locked_at = NULL,
|
|
||||||
updated_at = ?,
|
|
||||||
last_error = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
).run(timestamp, String(errorMessage || "Unknown async worker error"), id);
|
|
||||||
return "failed";
|
|
||||||
}
|
|
||||||
|
|
||||||
const backoffSeconds = Math.min(300, Math.max(1, 2 ** Math.max(0, attemptCount - 1)));
|
|
||||||
const nextRunAt = new Date(Date.now() + backoffSeconds * 1000).toISOString();
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
UPDATE async_jobs
|
|
||||||
SET status = 'retry',
|
|
||||||
next_run_at = ?,
|
|
||||||
lock_token = NULL,
|
|
||||||
locked_at = NULL,
|
|
||||||
updated_at = ?,
|
|
||||||
last_error = ?
|
|
||||||
WHERE id = ?
|
|
||||||
`
|
|
||||||
).run(nextRunAt, timestamp, String(errorMessage || "Unknown async worker error"), id);
|
|
||||||
return "retry";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reportSectionRepository = {
|
|
||||||
upsert({
|
|
||||||
workflowId,
|
|
||||||
sectionIndex,
|
|
||||||
sectionName,
|
|
||||||
downloadUrl,
|
|
||||||
filePath,
|
|
||||||
contentType,
|
|
||||||
rowCount,
|
|
||||||
headers,
|
|
||||||
sha256,
|
|
||||||
status,
|
|
||||||
errorMessage
|
|
||||||
}) {
|
|
||||||
const existing = db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT * FROM report_sections
|
|
||||||
WHERE workflow_id = ? AND section_index = ?
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.get(workflowId, sectionIndex);
|
|
||||||
const timestamp = nowIso();
|
|
||||||
const row = {
|
|
||||||
id: existing?.id || uuidv4(),
|
|
||||||
workflow_id: workflowId,
|
|
||||||
section_index: sectionIndex,
|
|
||||||
section_name: sectionName || null,
|
|
||||||
download_url: downloadUrl || null,
|
|
||||||
file_path: filePath || existing?.file_path || null,
|
|
||||||
content_type: contentType || null,
|
|
||||||
row_count: Number.isFinite(rowCount) ? rowCount : null,
|
|
||||||
headers_json: headers ? JSON.stringify(headers) : existing?.headers_json || null,
|
|
||||||
sha256: sha256 || null,
|
|
||||||
status: status || existing?.status || "queued",
|
|
||||||
error_message: errorMessage || null,
|
|
||||||
created_at: existing?.created_at || timestamp,
|
|
||||||
updated_at: timestamp
|
|
||||||
};
|
|
||||||
|
|
||||||
db.prepare(
|
|
||||||
`
|
|
||||||
INSERT INTO report_sections (
|
|
||||||
id, workflow_id, section_index, section_name, download_url, file_path, content_type,
|
|
||||||
row_count, headers_json, sha256, status, error_message, created_at, updated_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
@id, @workflow_id, @section_index, @section_name, @download_url, @file_path, @content_type,
|
|
||||||
@row_count, @headers_json, @sha256, @status, @error_message, @created_at, @updated_at
|
|
||||||
)
|
|
||||||
ON CONFLICT(workflow_id, section_index) DO UPDATE SET
|
|
||||||
section_name = excluded.section_name,
|
|
||||||
download_url = excluded.download_url,
|
|
||||||
file_path = excluded.file_path,
|
|
||||||
content_type = excluded.content_type,
|
|
||||||
row_count = excluded.row_count,
|
|
||||||
headers_json = excluded.headers_json,
|
|
||||||
sha256 = excluded.sha256,
|
|
||||||
status = excluded.status,
|
|
||||||
error_message = excluded.error_message,
|
|
||||||
updated_at = excluded.updated_at
|
|
||||||
`
|
|
||||||
).run(row);
|
|
||||||
|
|
||||||
return db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT *
|
|
||||||
FROM report_sections
|
|
||||||
WHERE workflow_id = ? AND section_index = ?
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.get(workflowId, sectionIndex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
merchantRepository,
|
merchantRepository,
|
||||||
uberConnectionRepository,
|
uberConnectionRepository,
|
||||||
webhookRepository,
|
webhookRepository,
|
||||||
apiLogRepository,
|
apiLogRepository,
|
||||||
appTokenRepository,
|
appTokenRepository,
|
||||||
tokenRequestLogRepository,
|
tokenRequestLogRepository
|
||||||
reportJobRepository,
|
|
||||||
asyncJobRepository,
|
|
||||||
reportSectionRepository
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -58,7 +58,6 @@ function initSchema() {
|
|||||||
headers_json TEXT,
|
headers_json TEXT,
|
||||||
received_at TEXT NOT NULL,
|
received_at TEXT NOT NULL,
|
||||||
processed_at TEXT,
|
processed_at TEXT,
|
||||||
last_error TEXT,
|
|
||||||
processing_status TEXT NOT NULL DEFAULT 'received',
|
processing_status TEXT NOT NULL DEFAULT 'received',
|
||||||
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
|
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
|
||||||
);
|
);
|
||||||
@ -98,64 +97,6 @@ function initSchema() {
|
|||||||
grant_type TEXT NOT NULL,
|
grant_type TEXT NOT NULL,
|
||||||
requested_at TEXT NOT NULL
|
requested_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS report_jobs (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
merchant_id TEXT,
|
|
||||||
workflow_id TEXT NOT NULL UNIQUE,
|
|
||||||
report_type TEXT NOT NULL,
|
|
||||||
store_uuids_json TEXT,
|
|
||||||
group_uuids_json TEXT,
|
|
||||||
start_date TEXT NOT NULL,
|
|
||||||
end_date TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
webhook_event_id TEXT,
|
|
||||||
webhook_msg_uuid TEXT,
|
|
||||||
report_sections_json TEXT,
|
|
||||||
webhook_payload_json TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
completed_at TEXT,
|
|
||||||
updated_at TEXT NOT NULL,
|
|
||||||
FOREIGN KEY(merchant_id) REFERENCES merchants(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS async_jobs (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
job_type TEXT NOT NULL,
|
|
||||||
payload_json TEXT NOT NULL,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
max_attempts INTEGER NOT NULL DEFAULT 7,
|
|
||||||
next_run_at TEXT NOT NULL,
|
|
||||||
last_error TEXT,
|
|
||||||
lock_token TEXT,
|
|
||||||
locked_at TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_async_jobs_run_window
|
|
||||||
ON async_jobs(status, next_run_at, created_at);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS report_sections (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
workflow_id TEXT NOT NULL,
|
|
||||||
section_index INTEGER NOT NULL,
|
|
||||||
section_name TEXT,
|
|
||||||
download_url TEXT,
|
|
||||||
file_path TEXT,
|
|
||||||
content_type TEXT,
|
|
||||||
row_count INTEGER,
|
|
||||||
headers_json TEXT,
|
|
||||||
sha256 TEXT,
|
|
||||||
status TEXT NOT NULL,
|
|
||||||
error_message TEXT,
|
|
||||||
created_at TEXT NOT NULL,
|
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_report_sections_workflow_section
|
|
||||||
ON report_sections(workflow_id, section_index);
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
if (!tableHasColumn("webhook_events", "resource_id")) {
|
if (!tableHasColumn("webhook_events", "resource_id")) {
|
||||||
@ -179,40 +120,6 @@ function initSchema() {
|
|||||||
if (!tableHasColumn("api_logs", "order_id")) {
|
if (!tableHasColumn("api_logs", "order_id")) {
|
||||||
db.exec("ALTER TABLE api_logs ADD COLUMN order_id TEXT");
|
db.exec("ALTER TABLE api_logs ADD COLUMN order_id TEXT");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tableHasColumn("uber_connections", "last_menu_refresh_requested_at")) {
|
|
||||||
db.exec("ALTER TABLE uber_connections ADD COLUMN last_menu_refresh_requested_at TEXT");
|
|
||||||
}
|
|
||||||
if (!tableHasColumn("uber_connections", "last_menu_refresh_webhook_uuid")) {
|
|
||||||
db.exec("ALTER TABLE uber_connections ADD COLUMN last_menu_refresh_webhook_uuid TEXT");
|
|
||||||
}
|
|
||||||
if (!tableHasColumn("uber_connections", "last_webhook_environment")) {
|
|
||||||
db.exec("ALTER TABLE uber_connections ADD COLUMN last_webhook_environment TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableHasColumn("report_jobs", "webhook_event_id")) {
|
|
||||||
db.exec("ALTER TABLE report_jobs ADD COLUMN webhook_event_id TEXT");
|
|
||||||
}
|
|
||||||
if (!tableHasColumn("report_jobs", "webhook_msg_uuid")) {
|
|
||||||
db.exec("ALTER TABLE report_jobs ADD COLUMN webhook_msg_uuid TEXT");
|
|
||||||
}
|
|
||||||
if (!tableHasColumn("report_jobs", "report_sections_json")) {
|
|
||||||
db.exec("ALTER TABLE report_jobs ADD COLUMN report_sections_json TEXT");
|
|
||||||
}
|
|
||||||
if (!tableHasColumn("report_jobs", "webhook_payload_json")) {
|
|
||||||
db.exec("ALTER TABLE report_jobs ADD COLUMN webhook_payload_json TEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableHasColumn("webhook_events", "processed_at")) {
|
|
||||||
db.exec("ALTER TABLE webhook_events ADD COLUMN processed_at TEXT");
|
|
||||||
}
|
|
||||||
if (!tableHasColumn("webhook_events", "processing_status")) {
|
|
||||||
db.exec("ALTER TABLE webhook_events ADD COLUMN processing_status TEXT");
|
|
||||||
db.exec("UPDATE webhook_events SET processing_status = 'received' WHERE processing_status IS NULL");
|
|
||||||
}
|
|
||||||
if (!tableHasColumn("webhook_events", "last_error")) {
|
|
||||||
db.exec("ALTER TABLE webhook_events ADD COLUMN last_error TEXT");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -121,23 +121,21 @@ function parseExpiresAt(expiresInSeconds) {
|
|||||||
return new Date(Date.now() + seconds * 1000).toISOString();
|
return new Date(Date.now() + seconds * 1000).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCachedClientCredentialsToken({ scope, forceRefresh = false } = {}) {
|
async function getCachedClientCredentialsToken({ scope }) {
|
||||||
const normalizedScope = (scope || AUTH_SCOPES.ORDER).trim().split(/\s+/).sort().join(" ");
|
const normalizedScope = (scope || AUTH_SCOPES.ORDER).trim().split(/\s+/).sort().join(" ");
|
||||||
if (!forceRefresh) {
|
const cached = appTokenRepository.findValid({
|
||||||
const cached = appTokenRepository.findValid({
|
provider: "uber",
|
||||||
provider: "uber",
|
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS,
|
||||||
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS,
|
scope: normalizedScope
|
||||||
scope: normalizedScope
|
});
|
||||||
});
|
if (cached) {
|
||||||
if (cached) {
|
return {
|
||||||
return {
|
access_token: cached.access_token,
|
||||||
access_token: cached.access_token,
|
token_type: cached.token_type,
|
||||||
token_type: cached.token_type,
|
scope: cached.scope,
|
||||||
scope: cached.scope,
|
expires_in: Math.max(0, Math.floor((new Date(cached.expires_at).getTime() - Date.now()) / 1000)),
|
||||||
expires_in: Math.max(0, Math.floor((new Date(cached.expires_at).getTime() - Date.now()) / 1000)),
|
source: "cache"
|
||||||
source: "cache"
|
};
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestsLastHour = tokenRequestLogRepository.countInLastHour({
|
const requestsLastHour = tokenRequestLogRepository.countInLastHour({
|
||||||
|
|||||||
@ -1,166 +0,0 @@
|
|||||||
function parseTimeToMinutes(time) {
|
|
||||||
if (typeof time !== "string" || !/^\d{2}:\d{2}$/.test(time)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const [hourStr, minuteStr] = time.split(":");
|
|
||||||
const hour = Number(hourStr);
|
|
||||||
const minute = Number(minuteStr);
|
|
||||||
if (
|
|
||||||
Number.isNaN(hour) ||
|
|
||||||
Number.isNaN(minute) ||
|
|
||||||
hour < 0 ||
|
|
||||||
hour > 23 ||
|
|
||||||
minute < 0 ||
|
|
||||||
minute > 59
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return hour * 60 + minute;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DAYS = [
|
|
||||||
"monday",
|
|
||||||
"tuesday",
|
|
||||||
"wednesday",
|
|
||||||
"thursday",
|
|
||||||
"friday",
|
|
||||||
"saturday",
|
|
||||||
"sunday"
|
|
||||||
];
|
|
||||||
|
|
||||||
function dayIndex(day) {
|
|
||||||
return DAYS.indexOf(String(day || "").toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildWeeklyIntervals(menuPayload) {
|
|
||||||
const menus = Array.isArray(menuPayload?.menus) ? menuPayload.menus : [];
|
|
||||||
const intervals = [];
|
|
||||||
|
|
||||||
menus.forEach((menu, menuIndex) => {
|
|
||||||
const availability = Array.isArray(menu?.service_availability) ? menu.service_availability : [];
|
|
||||||
availability.forEach((dayConfig, dayConfigIndex) => {
|
|
||||||
const dIdx = dayIndex(dayConfig?.day_of_week);
|
|
||||||
if (dIdx < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const periods = Array.isArray(dayConfig?.time_periods) ? dayConfig.time_periods : [];
|
|
||||||
periods.forEach((period, periodIndex) => {
|
|
||||||
const start = parseTimeToMinutes(period?.start_time);
|
|
||||||
const end = parseTimeToMinutes(period?.end_time);
|
|
||||||
if (start === null || end === null || end < start) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const base = dIdx * 24 * 60;
|
|
||||||
intervals.push({
|
|
||||||
start: base + start,
|
|
||||||
end: base + end,
|
|
||||||
menuIndex,
|
|
||||||
dayConfigIndex,
|
|
||||||
periodIndex,
|
|
||||||
day: dayConfig?.day_of_week
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return intervals.sort((a, b) => a.start - b.start);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeContiguousIntervals(intervals) {
|
|
||||||
if (!intervals.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const merged = [Object.assign({}, intervals[0])];
|
|
||||||
for (let i = 1; i < intervals.length; i += 1) {
|
|
||||||
const curr = intervals[i];
|
|
||||||
const last = merged[merged.length - 1];
|
|
||||||
if (curr.start <= last.end + 1) {
|
|
||||||
last.end = Math.max(last.end, curr.end);
|
|
||||||
} else {
|
|
||||||
merged.push(Object.assign({}, curr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectVisibilityOverlapErrors(menuPayload) {
|
|
||||||
const items = Array.isArray(menuPayload?.items) ? menuPayload.items : [];
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
items.forEach((item) => {
|
|
||||||
const visibilityHours = Array.isArray(item?.visibility_info?.hours)
|
|
||||||
? item.visibility_info.hours
|
|
||||||
: [];
|
|
||||||
visibilityHours.forEach((hourRule) => {
|
|
||||||
const hoursOfWeek = Array.isArray(hourRule?.hours_of_week) ? hourRule.hours_of_week : [];
|
|
||||||
const perDay = new Map();
|
|
||||||
hoursOfWeek.forEach((dayHours) => {
|
|
||||||
const d = String(dayHours?.day_of_week || "").toLowerCase();
|
|
||||||
if (!DAYS.includes(d)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const list = perDay.get(d) || [];
|
|
||||||
const periods = Array.isArray(dayHours?.time_periods) ? dayHours.time_periods : [];
|
|
||||||
periods.forEach((p) => {
|
|
||||||
const s = parseTimeToMinutes(p?.start_time);
|
|
||||||
const e = parseTimeToMinutes(p?.end_time);
|
|
||||||
if (s === null || e === null || e < s) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
list.push({ start: s, end: e, raw: p });
|
|
||||||
});
|
|
||||||
perDay.set(d, list);
|
|
||||||
});
|
|
||||||
|
|
||||||
perDay.forEach((periods, d) => {
|
|
||||||
const sorted = periods.sort((a, b) => a.start - b.start);
|
|
||||||
for (let i = 1; i < sorted.length; i += 1) {
|
|
||||||
const prev = sorted[i - 1];
|
|
||||||
const curr = sorted[i];
|
|
||||||
if (curr.start <= prev.end) {
|
|
||||||
errors.push(
|
|
||||||
`Invalid visibility overlap for item ${item?.id || "<unknown>"} on ${d}: ` +
|
|
||||||
`${String(prev.raw?.start_time)}-${String(prev.raw?.end_time)} overlaps ` +
|
|
||||||
`${String(curr.raw?.start_time)}-${String(curr.raw?.end_time)}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateUploadMenuPayload(menuPayload) {
|
|
||||||
const errors = [];
|
|
||||||
const menus = Array.isArray(menuPayload?.menus) ? menuPayload.menus : [];
|
|
||||||
if (menus.length === 0) {
|
|
||||||
errors.push("No Menus Errors: menu.menus must contain at least one menu.");
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const intervals = buildWeeklyIntervals(menuPayload);
|
|
||||||
if (intervals.length === 0) {
|
|
||||||
errors.push(
|
|
||||||
"No Hours Errors: at least one service_availability interval is required across the week."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const merged = mergeContiguousIntervals(intervals);
|
|
||||||
const shortBlocks = merged.filter((block) => block.end - block.start + 1 < 60);
|
|
||||||
if (shortBlocks.length > 0) {
|
|
||||||
errors.push(
|
|
||||||
"Short Hours Errors: each effective contiguous service_availability interval must be at least 60 minutes."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errors.push(...collectVisibilityOverlapErrors(menuPayload));
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
validateUploadMenuPayload
|
|
||||||
};
|
|
||||||
@ -1,7 +1,5 @@
|
|||||||
const { z } = require("zod");
|
const { z } = require("zod");
|
||||||
const proxyService = require("./proxy.service");
|
const proxyService = require("./proxy.service");
|
||||||
const { PRODUCT_TYPES, MIXIN_TYPES } = require("../../config/uberProductCatalog");
|
|
||||||
const { validateUploadMenuPayload } = require("./menuValidation");
|
|
||||||
|
|
||||||
const genericSchema = z.object({
|
const genericSchema = z.object({
|
||||||
merchantId: z.string().min(1).optional(),
|
merchantId: z.string().min(1).optional(),
|
||||||
@ -22,7 +20,7 @@ async function genericProxy(req, res) {
|
|||||||
async function upsertMenu(req, res) {
|
async function upsertMenu(req, res) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
merchantId: z.string().min(1),
|
merchantId: z.string().min(1),
|
||||||
storeId: z.string().uuid(),
|
storeId: z.string().min(1),
|
||||||
menu: z.any()
|
menu: z.any()
|
||||||
});
|
});
|
||||||
const payload = schema.parse(req.body);
|
const payload = schema.parse(req.body);
|
||||||
@ -37,58 +35,20 @@ async function upsertMenu(req, res) {
|
|||||||
async function getMenu(req, res) {
|
async function getMenu(req, res) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
merchantId: z.string().min(1),
|
merchantId: z.string().min(1),
|
||||||
storeId: z.string().uuid(),
|
storeId: z.string().min(1)
|
||||||
menu_type: z
|
|
||||||
.enum([
|
|
||||||
"MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"MENU_TYPE_FULFILLMENT_PICK_UP",
|
|
||||||
"MENU_TYPE_FULFILLMENT_DINE_IN"
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
menuType: z
|
|
||||||
.enum([
|
|
||||||
"MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"MENU_TYPE_FULFILLMENT_PICK_UP",
|
|
||||||
"MENU_TYPE_FULFILLMENT_DINE_IN"
|
|
||||||
])
|
|
||||||
.optional()
|
|
||||||
});
|
});
|
||||||
const payload = schema.parse(req.query);
|
const payload = schema.parse(req.query);
|
||||||
const data = await proxyService.menuGet({
|
const data = await proxyService.menuGet(payload);
|
||||||
merchantId: payload.merchantId,
|
|
||||||
storeId: payload.storeId,
|
|
||||||
menuType: payload.menu_type || payload.menuType
|
|
||||||
});
|
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replaceMenu(req, res) {
|
async function replaceMenu(req, res) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
merchantId: z.string().min(1),
|
merchantId: z.string().min(1),
|
||||||
storeId: z.string().uuid(),
|
storeId: z.string().min(1),
|
||||||
menu: z.any()
|
menu: z.any()
|
||||||
});
|
});
|
||||||
const payload = schema.parse(req.body);
|
const payload = schema.parse(req.body);
|
||||||
const allowedMenuTypes = new Set([
|
|
||||||
"MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"MENU_TYPE_FULFILLMENT_PICK_UP",
|
|
||||||
"MENU_TYPE_FULFILLMENT_DINE_IN"
|
|
||||||
]);
|
|
||||||
const menuType = payload.menu && payload.menu.menu_type;
|
|
||||||
if (menuType && !allowedMenuTypes.has(menuType)) {
|
|
||||||
const error = new Error(
|
|
||||||
"menu.menu_type must be one of MENU_TYPE_FULFILLMENT_DELIVERY, MENU_TYPE_FULFILLMENT_PICK_UP, MENU_TYPE_FULFILLMENT_DINE_IN"
|
|
||||||
);
|
|
||||||
error.status = 400;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
const uploadErrors = validateUploadMenuPayload(payload.menu || {});
|
|
||||||
if (uploadErrors.length > 0) {
|
|
||||||
const error = new Error(uploadErrors.join(" "));
|
|
||||||
error.status = 400;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await proxyService.menuReplace({
|
const data = await proxyService.menuReplace({
|
||||||
merchantId: payload.merchantId,
|
merchantId: payload.merchantId,
|
||||||
storeId: payload.storeId,
|
storeId: payload.storeId,
|
||||||
@ -98,60 +58,16 @@ async function replaceMenu(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateMenuItems(req, res) {
|
async function updateMenuItems(req, res) {
|
||||||
const menuTypeEnum = z.enum([
|
|
||||||
"MENU_TYPE_FULFILLMENT_DELIVERY",
|
|
||||||
"MENU_TYPE_FULFILLMENT_PICK_UP",
|
|
||||||
"MENU_TYPE_FULFILLMENT_DINE_IN"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const updateSchema = z
|
|
||||||
.object({
|
|
||||||
price_info: z
|
|
||||||
.object({
|
|
||||||
price: z.coerce.number().int().min(0).optional(),
|
|
||||||
core_price: z.coerce.number().int().min(0).optional(),
|
|
||||||
container_deposit: z.coerce.number().int().min(0).optional(),
|
|
||||||
overrides: z.array(z.any()).optional(),
|
|
||||||
priced_by_unit: z.any().optional()
|
|
||||||
})
|
|
||||||
.partial()
|
|
||||||
.optional(),
|
|
||||||
suspension_info: z.any().optional(),
|
|
||||||
menu_type: menuTypeEnum.optional(),
|
|
||||||
product_info: z.any().optional(),
|
|
||||||
classifications: z.any().optional(),
|
|
||||||
beverage_info: z.any().optional(),
|
|
||||||
physical_properties_info: z.any().optional(),
|
|
||||||
medication_info: z.any().optional(),
|
|
||||||
nutritional_info: z.any().optional(),
|
|
||||||
selling_info: z.any().optional()
|
|
||||||
})
|
|
||||||
.refine((value) => Object.keys(value).length > 0, {
|
|
||||||
message: "update must include at least one sparse-update field"
|
|
||||||
});
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
merchantId: z.string().min(1),
|
merchantId: z.string().min(1),
|
||||||
storeId: z.string().uuid(),
|
storeId: z.string().min(1),
|
||||||
itemId: z.string().min(1),
|
items: z.array(z.any()).min(1)
|
||||||
update: updateSchema
|
|
||||||
});
|
});
|
||||||
const payload = schema.parse(req.body);
|
const payload = schema.parse(req.body);
|
||||||
if (
|
|
||||||
payload.update.price_info &&
|
|
||||||
payload.update.price_info.core_price !== undefined &&
|
|
||||||
payload.update.price_info.price !== undefined &&
|
|
||||||
payload.update.price_info.core_price < payload.update.price_info.price
|
|
||||||
) {
|
|
||||||
const error = new Error("Invalid Price Info Errors: core_price must be greater than or equal to price.");
|
|
||||||
error.status = 400;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
const data = await proxyService.updateMenuItems({
|
const data = await proxyService.updateMenuItems({
|
||||||
merchantId: payload.merchantId,
|
merchantId: payload.merchantId,
|
||||||
storeId: payload.storeId,
|
storeId: payload.storeId,
|
||||||
itemId: payload.itemId,
|
payload: { items: payload.items }
|
||||||
payload: payload.update
|
|
||||||
});
|
});
|
||||||
return res.json({ success: true, data });
|
return res.json({ success: true, data });
|
||||||
}
|
}
|
||||||
@ -719,73 +635,6 @@ async function deliveryByocIngestCourierLocation(req, res) {
|
|||||||
return res.json({ success: true, data });
|
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 });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUberProductTypes(req, res) {
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
product_types: PRODUCT_TYPES,
|
|
||||||
mixin_types: MIXIN_TYPES
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
genericProxy,
|
genericProxy,
|
||||||
upsertMenu,
|
upsertMenu,
|
||||||
@ -827,10 +676,5 @@ module.exports = {
|
|||||||
deliveryResolveFulfillmentIssues,
|
deliveryResolveFulfillmentIssues,
|
||||||
deliveryGetReplacementRecommendations,
|
deliveryGetReplacementRecommendations,
|
||||||
deliveryUpdatePartnerCount,
|
deliveryUpdatePartnerCount,
|
||||||
deliveryByocIngestCourierLocation,
|
deliveryByocIngestCourierLocation
|
||||||
deliveryCreatePromotion,
|
|
||||||
deliveryRevokePromotion,
|
|
||||||
deliveryGetPromotion,
|
|
||||||
deliveryListPromotions,
|
|
||||||
getUberProductTypes
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,8 @@
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const zlib = require("zlib");
|
|
||||||
const { promisify } = require("util");
|
|
||||||
const env = require("../../config/env");
|
const env = require("../../config/env");
|
||||||
const uberEndpoints = require("../../config/uberEndpoints");
|
const uberEndpoints = require("../../config/uberEndpoints");
|
||||||
const { uberConnectionRepository, apiLogRepository, appTokenRepository } = require("../../db/adapter");
|
const { uberConnectionRepository, apiLogRepository } = require("../../db/adapter");
|
||||||
const {
|
const { getCachedClientCredentialsToken, AUTH_SCOPES } = require("../auth/auth.service");
|
||||||
getCachedClientCredentialsToken,
|
|
||||||
refreshToken,
|
|
||||||
AUTH_SCOPES,
|
|
||||||
AUTH_GRANT_TYPES
|
|
||||||
} = require("../auth/auth.service");
|
|
||||||
const { normalizeUberError, isRetryableUberError } = require("../common/errors/uberError");
|
const { normalizeUberError, isRetryableUberError } = require("../common/errors/uberError");
|
||||||
const { withExponentialBackoffRetry } = require("../common/http/retry");
|
const { withExponentialBackoffRetry } = require("../common/http/retry");
|
||||||
|
|
||||||
@ -18,8 +11,6 @@ const uberApiClient = axios.create({
|
|||||||
timeout: 30000
|
timeout: 30000
|
||||||
});
|
});
|
||||||
|
|
||||||
const gzipAsync = promisify(zlib.gzip);
|
|
||||||
|
|
||||||
function interpolatePath(pathTemplate, params = {}) {
|
function interpolatePath(pathTemplate, params = {}) {
|
||||||
let output = pathTemplate;
|
let output = pathTemplate;
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
@ -64,60 +55,11 @@ async function resolveAuthToken({ authMode = "app", merchantId, scopes }) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUnauthorizedError(error) {
|
async function callUberApi({ merchantId, method, uberPath, query, body, wrapperRoute, authMode, scopes }) {
|
||||||
return Number(error?.response?.status || 0) === 401;
|
const resolvedAuth = await resolveAuthToken({ authMode, merchantId, scopes });
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshMerchantConnectionToken(merchantId) {
|
try {
|
||||||
const connection = uberConnectionRepository.findByMerchantId(merchantId);
|
const response = await withExponentialBackoffRetry({
|
||||||
if (!connection) {
|
|
||||||
const error = new Error("Uber merchant connection not found for token refresh.");
|
|
||||||
error.status = 404;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (!connection.refresh_token) {
|
|
||||||
const error = new Error("Refresh token is not available for merchant OAuth connection.");
|
|
||||||
error.status = 401;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = await refreshToken(connection.refresh_token);
|
|
||||||
const expiresAt = tokenData.expires_in
|
|
||||||
? new Date(Date.now() + Number(tokenData.expires_in) * 1000).toISOString()
|
|
||||||
: connection.expires_at;
|
|
||||||
|
|
||||||
const updated = uberConnectionRepository.upsertByMerchantId(merchantId, {
|
|
||||||
accessToken: tokenData.access_token,
|
|
||||||
refreshToken: tokenData.refresh_token || connection.refresh_token,
|
|
||||||
tokenType: tokenData.token_type || connection.token_type || "Bearer",
|
|
||||||
scope: tokenData.scope || connection.scope || null,
|
|
||||||
expiresAt,
|
|
||||||
status: "active"
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
tokenType: updated.token_type || "Bearer",
|
|
||||||
accessToken: updated.access_token
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function callUberApi({
|
|
||||||
merchantId,
|
|
||||||
method,
|
|
||||||
uberPath,
|
|
||||||
query,
|
|
||||||
body,
|
|
||||||
logRequestBody,
|
|
||||||
wrapperRoute,
|
|
||||||
authMode,
|
|
||||||
scopes,
|
|
||||||
headers
|
|
||||||
}) {
|
|
||||||
const scopeKey = scopes || AUTH_SCOPES.ORDER;
|
|
||||||
const normalizedScope = scopeKey.trim().split(/\s+/).sort().join(" ");
|
|
||||||
let resolvedAuth = await resolveAuthToken({ authMode, merchantId, scopes: normalizedScope });
|
|
||||||
const buildRequest = () =>
|
|
||||||
withExponentialBackoffRetry({
|
|
||||||
fn: async () =>
|
fn: async () =>
|
||||||
uberApiClient.request({
|
uberApiClient.request({
|
||||||
method,
|
method,
|
||||||
@ -126,43 +68,13 @@ async function callUberApi({
|
|||||||
data: body,
|
data: body,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: buildAuthHeader(resolvedAuth.tokenType, resolvedAuth.accessToken),
|
Authorization: buildAuthHeader(resolvedAuth.tokenType, resolvedAuth.accessToken),
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
...(headers || {})
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
maxAttempts: 4,
|
maxAttempts: 4,
|
||||||
baseDelayMs: 300,
|
baseDelayMs: 300,
|
||||||
shouldRetry: (error) => isRetryableUberError(error)
|
shouldRetry: (error) => isRetryableUberError(error)
|
||||||
});
|
});
|
||||||
try {
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
response = await buildRequest();
|
|
||||||
} catch (error) {
|
|
||||||
if (!isUnauthorizedError(error)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authMode === "merchant" && merchantId) {
|
|
||||||
resolvedAuth = await refreshMerchantConnectionToken(merchantId);
|
|
||||||
} else {
|
|
||||||
appTokenRepository.deleteByProviderGrantScope({
|
|
||||||
provider: "uber",
|
|
||||||
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS,
|
|
||||||
scope: normalizedScope
|
|
||||||
});
|
|
||||||
const freshToken = await getCachedClientCredentialsToken({
|
|
||||||
scope: normalizedScope,
|
|
||||||
forceRefresh: true
|
|
||||||
});
|
|
||||||
resolvedAuth = {
|
|
||||||
tokenType: freshToken.token_type || "Bearer",
|
|
||||||
accessToken: freshToken.access_token
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
response = await buildRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
apiLogRepository.insert({
|
apiLogRepository.insert({
|
||||||
merchantId,
|
merchantId,
|
||||||
@ -170,7 +82,7 @@ async function callUberApi({
|
|||||||
wrapperRoute,
|
wrapperRoute,
|
||||||
uberPath,
|
uberPath,
|
||||||
responseStatus: response.status,
|
responseStatus: response.status,
|
||||||
requestBody: logRequestBody !== undefined ? logRequestBody : body,
|
requestBody: body,
|
||||||
responseBody: response.data
|
responseBody: response.data
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -184,7 +96,7 @@ async function callUberApi({
|
|||||||
wrapperRoute,
|
wrapperRoute,
|
||||||
uberPath,
|
uberPath,
|
||||||
responseStatus: normalized.status,
|
responseStatus: normalized.status,
|
||||||
requestBody: logRequestBody !== undefined ? logRequestBody : body,
|
requestBody: body,
|
||||||
responseBody: {
|
responseBody: {
|
||||||
code: normalized.code,
|
code: normalized.code,
|
||||||
message: normalized.message,
|
message: normalized.message,
|
||||||
@ -224,44 +136,32 @@ async function menuUpsert({ merchantId, storeId, payload }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function menuReplace({ merchantId, storeId, payload }) {
|
async function menuReplace({ merchantId, storeId, payload }) {
|
||||||
const uberPath = interpolatePath(uberEndpoints.menu.upload, { storeId });
|
const uberPath = interpolatePath(uberEndpoints.menu.upsert, { storeId });
|
||||||
const compressedPayload = await gzipAsync(Buffer.from(JSON.stringify(payload), "utf8"));
|
|
||||||
return callUberApi({
|
return callUberApi({
|
||||||
merchantId,
|
merchantId,
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
uberPath,
|
uberPath,
|
||||||
body: compressedPayload,
|
body: payload,
|
||||||
logRequestBody: payload,
|
|
||||||
headers: {
|
|
||||||
"Content-Encoding": "gzip",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
wrapperRoute: "/api/v1/uber/menu/replace",
|
wrapperRoute: "/api/v1/uber/menu/replace",
|
||||||
authMode: "app",
|
authMode: "app",
|
||||||
scopes: AUTH_SCOPES.STORE
|
scopes: AUTH_SCOPES.STORE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function menuGet({ merchantId, storeId, menuType }) {
|
async function menuGet({ merchantId, storeId }) {
|
||||||
const uberPath = interpolatePath(uberEndpoints.menu.get, { storeId });
|
const uberPath = interpolatePath(uberEndpoints.menu.get, { storeId });
|
||||||
return callUberApi({
|
return callUberApi({
|
||||||
merchantId,
|
merchantId,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
uberPath,
|
uberPath,
|
||||||
query: {
|
|
||||||
menu_type: menuType || "MENU_TYPE_FULFILLMENT_DELIVERY"
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
"Accept-Encoding": "gzip"
|
|
||||||
},
|
|
||||||
wrapperRoute: "/api/v1/uber/menu",
|
wrapperRoute: "/api/v1/uber/menu",
|
||||||
authMode: "app",
|
authMode: "app",
|
||||||
scopes: AUTH_SCOPES.STORE
|
scopes: AUTH_SCOPES.STORE
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateMenuItems({ merchantId, storeId, itemId, payload }) {
|
async function updateMenuItems({ merchantId, storeId, payload }) {
|
||||||
const uberPath = interpolatePath(uberEndpoints.menu.itemUpdate, { storeId, itemId });
|
const uberPath = interpolatePath(uberEndpoints.menu.itemsUpdate, { storeId });
|
||||||
return callUberApi({
|
return callUberApi({
|
||||||
merchantId,
|
merchantId,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -712,53 +612,6 @@ 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 = {
|
module.exports = {
|
||||||
genericProxy,
|
genericProxy,
|
||||||
menuUpsert,
|
menuUpsert,
|
||||||
@ -800,9 +653,5 @@ module.exports = {
|
|||||||
deliveryResolveFulfillmentIssues,
|
deliveryResolveFulfillmentIssues,
|
||||||
deliveryGetReplacementRecommendations,
|
deliveryGetReplacementRecommendations,
|
||||||
deliveryUpdatePartnerCount,
|
deliveryUpdatePartnerCount,
|
||||||
deliveryByocIngestCourierLocation,
|
deliveryByocIngestCourierLocation
|
||||||
deliveryCreatePromotion,
|
|
||||||
deliveryRevokePromotion,
|
|
||||||
deliveryGetPromotion,
|
|
||||||
deliveryListPromotions
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,34 +1,5 @@
|
|||||||
const { z } = require("zod");
|
const { z } = require("zod");
|
||||||
const { fetchReport, createReportJob } = require("./reporting.service");
|
const { fetchReport } = require("./reporting.service");
|
||||||
|
|
||||||
const REPORT_TYPES = [
|
|
||||||
"PAYMENT_DETAILS_REPORT",
|
|
||||||
"ORDER_ERRORS_MENU_ITEM_REPORT",
|
|
||||||
"ORDER_ERRORS_TRANSACTION_REPORT",
|
|
||||||
"ORDER_HISTORY_REPORT",
|
|
||||||
"DOWNTIME_REPORT",
|
|
||||||
"CUSTOMER_AND_DELIVERY_FEEDBACK_REPORT",
|
|
||||||
"MENU_ITEM_FEEDBACK_REPORT",
|
|
||||||
"BILLING_DETAILS_REPORT",
|
|
||||||
"ORDERS_AND_ITEMS_REPORT",
|
|
||||||
"FINANCE_SUMMARY_REPORT"
|
|
||||||
];
|
|
||||||
|
|
||||||
const RANGE_LIMIT_DAYS = {
|
|
||||||
PAYMENT_DETAILS_REPORT: 30,
|
|
||||||
ORDERS_AND_ITEMS_REPORT: 15,
|
|
||||||
FINANCE_SUMMARY_REPORT: 30
|
|
||||||
};
|
|
||||||
|
|
||||||
const LOOKBACK_RULES = {
|
|
||||||
ORDER_ERRORS_MENU_ITEM_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 },
|
|
||||||
ORDER_ERRORS_TRANSACTION_REPORT: { minDaysAgo: 190, maxDaysAgo: 4 },
|
|
||||||
ORDER_HISTORY_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 },
|
|
||||||
DOWNTIME_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 },
|
|
||||||
CUSTOMER_AND_DELIVERY_FEEDBACK_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 },
|
|
||||||
MENU_ITEM_FEEDBACK_REPORT: { minDaysAgo: 188, maxDaysAgo: 2 },
|
|
||||||
BILLING_DETAILS_REPORT: { minDaysAgo: 1825, maxDaysAgo: 2 }
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchSchema = z.object({
|
const fetchSchema = z.object({
|
||||||
method: z.enum(["GET", "POST"]).default("GET"),
|
method: z.enum(["GET", "POST"]).default("GET"),
|
||||||
@ -57,109 +28,7 @@ async function fetchReportingCsv(req, res) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDateLike(input, allowDateTime) {
|
|
||||||
const raw = String(input || "");
|
|
||||||
if (allowDateTime) {
|
|
||||||
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
||||||
return new Date(`${raw}T00:00:00.000Z`);
|
|
||||||
}
|
|
||||||
return new Date(raw);
|
|
||||||
}
|
|
||||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Date(`${raw}T00:00:00.000Z`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function daysBetweenInclusive(start, end) {
|
|
||||||
const msPerDay = 24 * 60 * 60 * 1000;
|
|
||||||
return Math.floor((end.getTime() - start.getTime()) / msPerDay) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function daysAgo(date) {
|
|
||||||
const now = new Date();
|
|
||||||
const utcNow = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
||||||
const utcDate = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
||||||
const msPerDay = 24 * 60 * 60 * 1000;
|
|
||||||
return Math.floor((utcNow.getTime() - utcDate.getTime()) / msPerDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createReportSchema = z
|
|
||||||
.object({
|
|
||||||
merchantId: z.string().min(1).optional(),
|
|
||||||
report_type: z.enum(REPORT_TYPES),
|
|
||||||
store_uuids: z.array(z.string().uuid()).optional(),
|
|
||||||
group_uuids: z.array(z.string().uuid()).optional(),
|
|
||||||
start_date: z.string().min(1),
|
|
||||||
end_date: z.string().min(1)
|
|
||||||
})
|
|
||||||
.refine((value) => (value.store_uuids?.length || 0) + (value.group_uuids?.length || 0) > 0, {
|
|
||||||
message: "At least one store_uuids or group_uuids value is required."
|
|
||||||
});
|
|
||||||
|
|
||||||
function validateCreateReportConstraints(payload) {
|
|
||||||
const allowDateTime = payload.report_type === "PAYMENT_DETAILS_REPORT";
|
|
||||||
const start = parseDateLike(payload.start_date, allowDateTime);
|
|
||||||
const end = parseDateLike(payload.end_date, allowDateTime);
|
|
||||||
if (!start || Number.isNaN(start.getTime()) || !end || Number.isNaN(end.getTime())) {
|
|
||||||
const error = new Error("Invalid report date format for selected report_type.");
|
|
||||||
error.status = 400;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (start.getTime() > end.getTime()) {
|
|
||||||
const error = new Error("start_date must be on or before end_date.");
|
|
||||||
error.status = 414;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rangeLimit = RANGE_LIMIT_DAYS[payload.report_type];
|
|
||||||
if (rangeLimit) {
|
|
||||||
const rangeDays = daysBetweenInclusive(start, end);
|
|
||||||
if (rangeDays > rangeLimit) {
|
|
||||||
const error = new Error(
|
|
||||||
`${payload.report_type} supports a maximum range period of ${rangeLimit} days.`
|
|
||||||
);
|
|
||||||
error.status = 416;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lookback = LOOKBACK_RULES[payload.report_type];
|
|
||||||
if (lookback) {
|
|
||||||
const startAgo = daysAgo(start);
|
|
||||||
const endAgo = daysAgo(end);
|
|
||||||
const startOutOfWindow = startAgo < lookback.maxDaysAgo || startAgo > lookback.minDaysAgo;
|
|
||||||
const endOutOfWindow = endAgo < lookback.maxDaysAgo || endAgo > lookback.minDaysAgo;
|
|
||||||
if (startOutOfWindow || endOutOfWindow) {
|
|
||||||
const error = new Error(
|
|
||||||
`${payload.report_type} must be within [T-${lookback.minDaysAgo}, T-${lookback.maxDaysAgo}] days.`
|
|
||||||
);
|
|
||||||
error.status = 416;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createMarketplaceReport(req, res) {
|
|
||||||
const payload = createReportSchema.parse(req.body || {});
|
|
||||||
validateCreateReportConstraints(payload);
|
|
||||||
|
|
||||||
const data = await createReportJob({
|
|
||||||
merchantId: payload.merchantId || null,
|
|
||||||
reportType: payload.report_type,
|
|
||||||
storeUuids: payload.store_uuids || [],
|
|
||||||
groupUuids: payload.group_uuids || [],
|
|
||||||
startDate: payload.start_date,
|
|
||||||
endDate: payload.end_date
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fetchReportingCsv,
|
fetchReportingCsv
|
||||||
createMarketplaceReport
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const env = require("../../config/env");
|
const env = require("../../config/env");
|
||||||
const { apiLogRepository, reportJobRepository, appTokenRepository } = require("../../db/adapter");
|
const { apiLogRepository } = require("../../db/adapter");
|
||||||
const {
|
const { getCachedClientCredentialsToken, AUTH_SCOPES } = require("../auth/auth.service");
|
||||||
getCachedClientCredentialsToken,
|
|
||||||
AUTH_SCOPES,
|
|
||||||
AUTH_GRANT_TYPES
|
|
||||||
} = require("../auth/auth.service");
|
|
||||||
const { normalizeUberError, isRetryableUberError } = require("../common/errors/uberError");
|
const { normalizeUberError, isRetryableUberError } = require("../common/errors/uberError");
|
||||||
const { withExponentialBackoffRetry } = require("../common/http/retry");
|
const { withExponentialBackoffRetry } = require("../common/http/retry");
|
||||||
|
|
||||||
@ -18,43 +14,6 @@ function buildAuthorizationHeader(tokenType, accessToken) {
|
|||||||
return `${tokenType || "Bearer"} ${accessToken}`;
|
return `${tokenType || "Bearer"} ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUnauthorizedError(error) {
|
|
||||||
return Number(error?.response?.status || 0) === 401;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeWithReportTokenRetry({ requestFactory }) {
|
|
||||||
const scope = AUTH_SCOPES.REPORT;
|
|
||||||
let token = await getCachedClientCredentialsToken({ scope });
|
|
||||||
|
|
||||||
const run = async () =>
|
|
||||||
withExponentialBackoffRetry({
|
|
||||||
fn: async () => requestFactory(token),
|
|
||||||
maxAttempts: 4,
|
|
||||||
baseDelayMs: 400,
|
|
||||||
shouldRetry: (error) => isRetryableUberError(error)
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await run();
|
|
||||||
} catch (error) {
|
|
||||||
if (!isUnauthorizedError(error)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
appTokenRepository.deleteByProviderGrantScope({
|
|
||||||
provider: "uber",
|
|
||||||
grantType: AUTH_GRANT_TYPES.CLIENT_CREDENTIALS,
|
|
||||||
scope
|
|
||||||
});
|
|
||||||
token = await getCachedClientCredentialsToken({
|
|
||||||
scope,
|
|
||||||
forceRefresh: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCsvLine(line) {
|
function parseCsvLine(line) {
|
||||||
const output = [];
|
const output = [];
|
||||||
let current = "";
|
let current = "";
|
||||||
@ -130,11 +89,14 @@ async function fetchReport({
|
|||||||
requiredHeaders = [],
|
requiredHeaders = [],
|
||||||
wrapperRoute = "/api/v1/uber/reporting/fetch"
|
wrapperRoute = "/api/v1/uber/reporting/fetch"
|
||||||
}) {
|
}) {
|
||||||
|
const token = await getCachedClientCredentialsToken({
|
||||||
|
scope: AUTH_SCOPES.REPORT
|
||||||
|
});
|
||||||
const requestMethod = String(method || "GET").toUpperCase();
|
const requestMethod = String(method || "GET").toUpperCase();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await executeWithReportTokenRetry({
|
const response = await withExponentialBackoffRetry({
|
||||||
requestFactory: async (token) =>
|
fn: async () =>
|
||||||
reportingClient.request({
|
reportingClient.request({
|
||||||
method: requestMethod,
|
method: requestMethod,
|
||||||
url: upstreamPath,
|
url: upstreamPath,
|
||||||
@ -145,7 +107,10 @@ async function fetchReport({
|
|||||||
Authorization: buildAuthorizationHeader(token.token_type, token.access_token),
|
Authorization: buildAuthorizationHeader(token.token_type, token.access_token),
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
maxAttempts: 4,
|
||||||
|
baseDelayMs: 400,
|
||||||
|
shouldRetry: (error) => isRetryableUberError(error)
|
||||||
});
|
});
|
||||||
|
|
||||||
const textData =
|
const textData =
|
||||||
@ -190,86 +155,6 @@ async function fetchReport({
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
async createReportJob({
|
|
||||||
merchantId,
|
|
||||||
reportType,
|
|
||||||
storeUuids,
|
|
||||||
groupUuids,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
}) {
|
|
||||||
const body = {
|
|
||||||
report_type: reportType,
|
|
||||||
store_uuids: storeUuids || [],
|
|
||||||
group_uuids: groupUuids || [],
|
|
||||||
start_date: startDate,
|
|
||||||
end_date: endDate
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await executeWithReportTokenRetry({
|
|
||||||
requestFactory: async (token) =>
|
|
||||||
reportingClient.request({
|
|
||||||
method: "POST",
|
|
||||||
url: "/v1/eats/report",
|
|
||||||
data: body,
|
|
||||||
headers: {
|
|
||||||
Authorization: buildAuthorizationHeader(token.token_type, token.access_token),
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const workflowId = response?.data?.workflow_id;
|
|
||||||
if (!workflowId) {
|
|
||||||
const error = new Error("Missing workflow_id in report creation response.");
|
|
||||||
error.status = 502;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
reportJobRepository.createRequested({
|
|
||||||
merchantId,
|
|
||||||
workflowId,
|
|
||||||
reportType,
|
|
||||||
storeUuids,
|
|
||||||
groupUuids,
|
|
||||||
startDate,
|
|
||||||
endDate
|
|
||||||
});
|
|
||||||
|
|
||||||
apiLogRepository.insert({
|
|
||||||
merchantId,
|
|
||||||
method: "POST",
|
|
||||||
wrapperRoute: "/api/v1/uber/reporting/create",
|
|
||||||
uberPath: "/v1/eats/report",
|
|
||||||
responseStatus: response.status,
|
|
||||||
requestBody: body,
|
|
||||||
responseBody: response.data
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
workflow_id: workflowId
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const normalized = normalizeUberError(error);
|
|
||||||
apiLogRepository.insert({
|
|
||||||
merchantId,
|
|
||||||
method: "POST",
|
|
||||||
wrapperRoute: "/api/v1/uber/reporting/create",
|
|
||||||
uberPath: "/v1/eats/report",
|
|
||||||
responseStatus: normalized.status,
|
|
||||||
requestBody: body,
|
|
||||||
responseBody: {
|
|
||||||
code: normalized.code,
|
|
||||||
message: normalized.message,
|
|
||||||
transient: normalized.transient,
|
|
||||||
details:
|
|
||||||
typeof normalized.details === "string" ? { raw: normalized.details } : normalized.details
|
|
||||||
}
|
|
||||||
});
|
|
||||||
throw normalized;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fetchReport,
|
fetchReport,
|
||||||
parseCsvByHeader
|
parseCsvByHeader
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,134 +0,0 @@
|
|||||||
const {
|
|
||||||
webhookRepository,
|
|
||||||
uberConnectionRepository,
|
|
||||||
reportJobRepository,
|
|
||||||
asyncJobRepository
|
|
||||||
} = require("../../db/adapter");
|
|
||||||
|
|
||||||
function parseJson(rawValue) {
|
|
||||||
if (!rawValue) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (typeof rawValue === "object") {
|
|
||||||
return rawValue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(rawValue);
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractStoreId(payload) {
|
|
||||||
return payload?.user_id || payload?.store_id || payload?.resource_id || payload?.store?.id || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickSectionUrl(section = {}) {
|
|
||||||
return (
|
|
||||||
section.url ||
|
|
||||||
section.download_url ||
|
|
||||||
section.signed_url ||
|
|
||||||
section.resource_url ||
|
|
||||||
section.href ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function enqueueReportSectionJobs(workflowId, sections) {
|
|
||||||
(sections || []).forEach((section, index) => {
|
|
||||||
const url = pickSectionUrl(section);
|
|
||||||
if (!url) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
asyncJobRepository.enqueue({
|
|
||||||
jobType: "report_section_download",
|
|
||||||
payload: {
|
|
||||||
workflowId,
|
|
||||||
sectionIndex: index,
|
|
||||||
section
|
|
||||||
},
|
|
||||||
maxAttempts: 7
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyProvisioningStateFromWebhook(eventType, payload) {
|
|
||||||
if (eventType !== "store.provisioned" && eventType !== "store.deprovisioned") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const storeId = extractStoreId(payload);
|
|
||||||
if (!storeId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const connection = uberConnectionRepository.findByUberStoreId(String(storeId));
|
|
||||||
if (!connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStatus = eventType === "store.provisioned" ? "active" : "deprovisioned";
|
|
||||||
uberConnectionRepository.setStatusByMerchantId(connection.merchant_id, nextStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMenuRefreshRequestFromWebhook(eventType, payload, headers) {
|
|
||||||
if (eventType !== "store.menu_refresh_request") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storeId = extractStoreId(payload);
|
|
||||||
if (!storeId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uberConnectionRepository.markMenuRefreshRequestedByStoreId(String(storeId), {
|
|
||||||
requestedAt: new Date().toISOString(),
|
|
||||||
webhookMsgUuid: payload?.webhook_meta?.webhook_msg_uuid || null,
|
|
||||||
environment: headers?.["x-environment"] || null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyReportSuccessFromWebhook(eventType, payload) {
|
|
||||||
if (eventType !== "eats.report.success") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workflowId = payload?.job_id;
|
|
||||||
if (!workflowId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sections = payload?.report_metadata?.sections || [];
|
|
||||||
reportJobRepository.markSuccessFromWebhook({
|
|
||||||
workflowId: String(workflowId),
|
|
||||||
eventId: payload?.event_id || null,
|
|
||||||
webhookMsgUuid: payload?.webhook_meta?.webhook_msg_uuid || null,
|
|
||||||
reportType: payload?.report_type || null,
|
|
||||||
sections,
|
|
||||||
payload
|
|
||||||
});
|
|
||||||
enqueueReportSectionJobs(String(workflowId), sections);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processWebhookEventById(eventId) {
|
|
||||||
const event = webhookRepository.findById(eventId);
|
|
||||||
if (!event) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.processing_status === "processed") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = parseJson(event.payload_json);
|
|
||||||
const headers = parseJson(event.headers_json);
|
|
||||||
const eventType = event.event_type || payload?.event_type || payload?.type || "unknown";
|
|
||||||
|
|
||||||
applyProvisioningStateFromWebhook(eventType, payload);
|
|
||||||
applyMenuRefreshRequestFromWebhook(eventType, payload, headers);
|
|
||||||
applyReportSuccessFromWebhook(eventType, payload);
|
|
||||||
webhookRepository.markProcessed(eventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
processWebhookEventById,
|
|
||||||
pickSectionUrl
|
|
||||||
};
|
|
||||||
@ -1,9 +1,6 @@
|
|||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
const env = require("../../config/env");
|
const env = require("../../config/env");
|
||||||
const {
|
const { webhookRepository, uberConnectionRepository } = require("../../db/adapter");
|
||||||
webhookRepository,
|
|
||||||
asyncJobRepository
|
|
||||||
} = require("../../db/adapter");
|
|
||||||
|
|
||||||
function getSignatureFromHeaders(headers) {
|
function getSignatureFromHeaders(headers) {
|
||||||
const signature = headers["x-uber-signature"];
|
const signature = headers["x-uber-signature"];
|
||||||
@ -68,6 +65,28 @@ function buildDedupeKey(signature, req) {
|
|||||||
return crypto.createHash("sha256").update(basis).digest("hex");
|
return crypto.createHash("sha256").update(basis).digest("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractStoreId(payload) {
|
||||||
|
return payload?.user_id || payload?.store_id || payload?.resource_id || payload?.store?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyProvisioningStateFromWebhook(eventType, payload) {
|
||||||
|
if (eventType !== "store.provisioned" && eventType !== "store.deprovisioned") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const storeId = extractStoreId(payload);
|
||||||
|
if (!storeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = uberConnectionRepository.findByUberStoreId(String(storeId));
|
||||||
|
if (!connection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStatus = eventType === "store.provisioned" ? "active" : "deprovisioned";
|
||||||
|
uberConnectionRepository.setStatusByMerchantId(connection.merchant_id, nextStatus);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleUberWebhook(req, res) {
|
async function handleUberWebhook(req, res) {
|
||||||
if (!verifyBasicAuthIfConfigured(req)) {
|
if (!verifyBasicAuthIfConfigured(req)) {
|
||||||
return res.status(401).json({
|
return res.status(401).json({
|
||||||
@ -101,7 +120,7 @@ async function handleUberWebhook(req, res) {
|
|||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
const inserted = webhookRepository.insert({
|
webhookRepository.insert({
|
||||||
provider: "uber",
|
provider: "uber",
|
||||||
merchantId,
|
merchantId,
|
||||||
eventType,
|
eventType,
|
||||||
@ -113,13 +132,7 @@ async function handleUberWebhook(req, res) {
|
|||||||
headersJson: req.headers
|
headersJson: req.headers
|
||||||
});
|
});
|
||||||
|
|
||||||
asyncJobRepository.enqueue({
|
applyProvisioningStateFromWebhook(eventType, req.body || {});
|
||||||
jobType: "uber_webhook_event",
|
|
||||||
payload: {
|
|
||||||
eventId: inserted.id
|
|
||||||
},
|
|
||||||
maxAttempts: 7
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,19 +17,6 @@ const router = express.Router();
|
|||||||
*/
|
*/
|
||||||
router.post("/uber/request", asyncHandler(controller.genericProxy));
|
router.post("/uber/request", asyncHandler(controller.genericProxy));
|
||||||
|
|
||||||
/**
|
|
||||||
* @openapi
|
|
||||||
* /api/v1/uber/catalog/product-types:
|
|
||||||
* get:
|
|
||||||
* summary: Return Uber Product Types and Uber Mixin Types catalog
|
|
||||||
* tags:
|
|
||||||
* - Uber Menu
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Catalog returned
|
|
||||||
*/
|
|
||||||
router.get("/uber/catalog/product-types", asyncHandler(controller.getUberProductTypes));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
* /api/v1/uber/menu/upsert:
|
* /api/v1/uber/menu/upsert:
|
||||||
@ -47,12 +34,12 @@ router.post("/uber/menu/upsert", asyncHandler(controller.upsertMenu));
|
|||||||
* @openapi
|
* @openapi
|
||||||
* /api/v1/uber/menu/replace:
|
* /api/v1/uber/menu/replace:
|
||||||
* put:
|
* put:
|
||||||
* summary: Upload/replace store menu (PUT /v2/eats/stores/{store_id}/menus)
|
* summary: Replace store menu (full upload)
|
||||||
* tags:
|
* tags:
|
||||||
* - Uber Menu
|
* - Uber Menu
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Menu replaced (Uber returns 204 No Content)
|
* description: Menu replaced
|
||||||
*/
|
*/
|
||||||
router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu));
|
router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu));
|
||||||
|
|
||||||
@ -60,12 +47,12 @@ router.put("/uber/menu/replace", asyncHandler(controller.replaceMenu));
|
|||||||
* @openapi
|
* @openapi
|
||||||
* /api/v1/uber/menu/items:
|
* /api/v1/uber/menu/items:
|
||||||
* post:
|
* post:
|
||||||
* summary: Update single menu item (POST /v2/eats/stores/{store_id}/menus/items/{item_id})
|
* summary: Update individual menu items (stock/price updates)
|
||||||
* tags:
|
* tags:
|
||||||
* - Uber Menu
|
* - Uber Menu
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Menu item sparsely updated (Uber returns 204 No Content)
|
* description: Menu items updated
|
||||||
*/
|
*/
|
||||||
router.post("/uber/menu/items", asyncHandler(controller.updateMenuItems));
|
router.post("/uber/menu/items", asyncHandler(controller.updateMenuItems));
|
||||||
|
|
||||||
@ -76,27 +63,6 @@ router.post("/uber/menu/items", asyncHandler(controller.updateMenuItems));
|
|||||||
* summary: Fetch store menu
|
* summary: Fetch store menu
|
||||||
* tags:
|
* tags:
|
||||||
* - Uber Menu
|
* - Uber Menu
|
||||||
* parameters:
|
|
||||||
* - in: query
|
|
||||||
* name: merchantId
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* - in: query
|
|
||||||
* name: storeId
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* - in: query
|
|
||||||
* name: menu_type
|
|
||||||
* required: false
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* enum:
|
|
||||||
* - MENU_TYPE_FULFILLMENT_DELIVERY
|
|
||||||
* - MENU_TYPE_FULFILLMENT_PICK_UP
|
|
||||||
* - MENU_TYPE_FULFILLMENT_DINE_IN
|
|
||||||
* description: Defaults to MENU_TYPE_FULFILLMENT_DELIVERY.
|
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Menu fetched
|
* description: Menu fetched
|
||||||
@ -722,87 +688,4 @@ router.post(
|
|||||||
asyncHandler(controller.deliveryByocIngestCourierLocation)
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const asyncHandler = require("../middleware/asyncHandler");
|
const asyncHandler = require("../middleware/asyncHandler");
|
||||||
const {
|
const { fetchReportingCsv } = require("../modules/reporting/reporting.controller");
|
||||||
fetchReportingCsv,
|
|
||||||
createMarketplaceReport
|
|
||||||
} = require("../modules/reporting/reporting.controller");
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@ -20,17 +17,5 @@ const router = express.Router();
|
|||||||
*/
|
*/
|
||||||
router.post("/uber/reporting/fetch", asyncHandler(fetchReportingCsv));
|
router.post("/uber/reporting/fetch", asyncHandler(fetchReportingCsv));
|
||||||
|
|
||||||
/**
|
|
||||||
* @openapi
|
|
||||||
* /api/v1/uber/reporting/create:
|
|
||||||
* post:
|
|
||||||
* summary: Create Marketplace Reporting API job (POST /v1/eats/report)
|
|
||||||
* tags:
|
|
||||||
* - Uber Reporting
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Report creation requested successfully (workflow_id returned)
|
|
||||||
*/
|
|
||||||
router.post("/uber/reporting/create", asyncHandler(createMarketplaceReport));
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
const app = require("./app");
|
const app = require("./app");
|
||||||
const env = require("./config/env");
|
const env = require("./config/env");
|
||||||
const { initSchema } = require("./db/sqlite");
|
const { initSchema } = require("./db/sqlite");
|
||||||
const { startAsyncWorkers } = require("./workers/asyncWorkers");
|
|
||||||
|
|
||||||
initSchema();
|
initSchema();
|
||||||
startAsyncWorkers();
|
|
||||||
|
|
||||||
app.listen(env.PORT, () => {
|
app.listen(env.PORT, () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Uber Wrapper listening on http://localhost:${env.PORT}`);
|
console.log(`Uber Wrapper listening on http://localhost:${env.PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,198 +0,0 @@
|
|||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const crypto = require("crypto");
|
|
||||||
const axios = require("axios");
|
|
||||||
const { v4: uuidv4 } = require("uuid");
|
|
||||||
const env = require("../config/env");
|
|
||||||
const { asyncJobRepository, reportSectionRepository, webhookRepository } = require("../db/adapter");
|
|
||||||
const { processWebhookEventById, pickSectionUrl } = require("../modules/webhooks/webhookProcessor");
|
|
||||||
const { parseCsvByHeader } = require("../modules/reporting/reporting.service");
|
|
||||||
|
|
||||||
let workerTimer = null;
|
|
||||||
let isProcessing = false;
|
|
||||||
|
|
||||||
function ensureDir(dirPath) {
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseJson(raw) {
|
|
||||||
if (!raw) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if (typeof raw === "object") {
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeSectionName(input, index) {
|
|
||||||
const fallback = `section-${index + 1}`;
|
|
||||||
const name = String(input || fallback)
|
|
||||||
.trim()
|
|
||||||
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
||||||
.slice(0, 80);
|
|
||||||
return name || fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeSha256(buffer) {
|
|
||||||
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleWebhookJob(payload) {
|
|
||||||
const eventId = payload?.eventId;
|
|
||||||
if (!eventId) {
|
|
||||||
throw new Error("Missing eventId in webhook async job payload.");
|
|
||||||
}
|
|
||||||
await processWebhookEventById(String(eventId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadSectionFile({ workflowId, sectionIndex, section }) {
|
|
||||||
const sectionUrl = pickSectionUrl(section);
|
|
||||||
if (!sectionUrl) {
|
|
||||||
throw new Error(`Missing section URL for workflow ${workflowId} section ${sectionIndex}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseDir = env.REPORT_DOWNLOAD_DIR;
|
|
||||||
const workflowDir = path.join(baseDir, String(workflowId));
|
|
||||||
ensureDir(baseDir);
|
|
||||||
ensureDir(workflowDir);
|
|
||||||
|
|
||||||
const sectionName = safeSectionName(section?.name || section?.section_name, sectionIndex);
|
|
||||||
const filePath = path.join(workflowDir, `${String(sectionIndex + 1).padStart(2, "0")}-${sectionName}.csv`);
|
|
||||||
|
|
||||||
const response = await axios.get(sectionUrl, {
|
|
||||||
responseType: "arraybuffer",
|
|
||||||
timeout: 60000
|
|
||||||
});
|
|
||||||
|
|
||||||
const bodyBuffer = Buffer.from(response.data || []);
|
|
||||||
fs.writeFileSync(filePath, bodyBuffer);
|
|
||||||
|
|
||||||
const csvText = bodyBuffer.toString("utf8");
|
|
||||||
const parsed = parseCsvByHeader(csvText);
|
|
||||||
const sha256 = computeSha256(bodyBuffer);
|
|
||||||
const contentType = response.headers?.["content-type"] || "text/csv";
|
|
||||||
|
|
||||||
reportSectionRepository.upsert({
|
|
||||||
workflowId: String(workflowId),
|
|
||||||
sectionIndex: Number(sectionIndex),
|
|
||||||
sectionName,
|
|
||||||
downloadUrl: sectionUrl,
|
|
||||||
filePath,
|
|
||||||
contentType,
|
|
||||||
rowCount: parsed?.rows?.length || 0,
|
|
||||||
headers: parsed?.headers || [],
|
|
||||||
sha256,
|
|
||||||
status: "completed",
|
|
||||||
errorMessage: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleReportSectionJob(payload) {
|
|
||||||
const workflowId = payload?.workflowId;
|
|
||||||
const sectionIndex = payload?.sectionIndex;
|
|
||||||
const section = payload?.section;
|
|
||||||
if (!workflowId && workflowId !== 0) {
|
|
||||||
throw new Error("Missing workflowId in report section download job.");
|
|
||||||
}
|
|
||||||
if (sectionIndex === undefined || sectionIndex === null) {
|
|
||||||
throw new Error("Missing sectionIndex in report section download job.");
|
|
||||||
}
|
|
||||||
await downloadSectionFile({
|
|
||||||
workflowId,
|
|
||||||
sectionIndex,
|
|
||||||
section
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processJob(job) {
|
|
||||||
const payload = parseJson(job.payload_json);
|
|
||||||
if (job.job_type === "uber_webhook_event") {
|
|
||||||
await handleWebhookJob(payload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (job.job_type === "report_section_download") {
|
|
||||||
await handleReportSectionJob(payload);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`Unsupported async job type: ${job.job_type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processBatch() {
|
|
||||||
if (isProcessing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isProcessing = true;
|
|
||||||
const workerId = `worker-${uuidv4()}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < env.ASYNC_WORKER_BATCH_SIZE; i += 1) {
|
|
||||||
const job = asyncJobRepository.claimNext({ workerId });
|
|
||||||
if (!job) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await processJob(job);
|
|
||||||
asyncJobRepository.markCompleted(job.id);
|
|
||||||
} catch (error) {
|
|
||||||
asyncJobRepository.markFailedOrRetry({
|
|
||||||
id: job.id,
|
|
||||||
attemptCount: Number(job.attempt_count || 0),
|
|
||||||
maxAttempts: Number(job.max_attempts || 1),
|
|
||||||
errorMessage: error?.message || String(error)
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = parseJson(job.payload_json);
|
|
||||||
if (job.job_type === "uber_webhook_event" && payload?.eventId) {
|
|
||||||
webhookRepository.markFailed(payload.eventId, error?.message || String(error));
|
|
||||||
}
|
|
||||||
if (job.job_type === "report_section_download") {
|
|
||||||
reportSectionRepository.upsert({
|
|
||||||
workflowId: String(payload?.workflowId || ""),
|
|
||||||
sectionIndex: Number(payload?.sectionIndex || 0),
|
|
||||||
sectionName: safeSectionName(payload?.section?.name, Number(payload?.sectionIndex || 0)),
|
|
||||||
downloadUrl: pickSectionUrl(payload?.section || {}),
|
|
||||||
status: "failed",
|
|
||||||
errorMessage: error?.message || String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isProcessing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startAsyncWorkers() {
|
|
||||||
if (!env.ASYNC_WORKERS_ENABLED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (workerTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
workerTimer = setInterval(() => {
|
|
||||||
processBatch().catch(() => {
|
|
||||||
// keep worker alive even when one polling cycle fails
|
|
||||||
});
|
|
||||||
}, env.ASYNC_WORKER_POLL_INTERVAL_MS);
|
|
||||||
workerTimer.unref?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAsyncWorkers() {
|
|
||||||
if (workerTimer) {
|
|
||||||
clearInterval(workerTimer);
|
|
||||||
workerTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
startAsyncWorkers,
|
|
||||||
stopAsyncWorkers
|
|
||||||
};
|
|
||||||
Loading…
x
Reference in New Issue
Block a user