doordash-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +180 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +154 -0
- package/dist/direct-api.d.ts +530 -0
- package/dist/direct-api.js +2297 -0
- package/dist/direct-api.test.d.ts +1 -0
- package/dist/direct-api.test.js +701 -0
- package/dist/lib.d.ts +21 -0
- package/dist/lib.js +202 -0
- package/docs/examples.md +158 -0
- package/docs/install.md +63 -0
- package/man/dd-cli.1 +241 -0
- package/man/doordash-cli.1 +1 -0
- package/package.json +70 -0
- package/scripts/install-manpage.sh +14 -0
|
@@ -0,0 +1,2297 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
6
|
+
import { chromium } from "playwright";
|
|
7
|
+
import { getCookiesPath } from "@striderlabs/mcp-doordash/dist/auth.js";
|
|
8
|
+
const BASE_URL = "https://www.doordash.com";
|
|
9
|
+
const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
|
|
10
|
+
const GRAPHQL_HEADERS = {
|
|
11
|
+
accept: "*/*",
|
|
12
|
+
"content-type": "application/json",
|
|
13
|
+
"x-channel-id": "marketplace",
|
|
14
|
+
"x-experience-id": "doordash",
|
|
15
|
+
"apollographql-client-name": "@doordash/app-consumer-production-ssr-client",
|
|
16
|
+
"apollographql-client-version": "3.0",
|
|
17
|
+
};
|
|
18
|
+
const CONSUMER_QUERY = `query consumer {
|
|
19
|
+
consumer {
|
|
20
|
+
id
|
|
21
|
+
userId
|
|
22
|
+
firstName
|
|
23
|
+
lastName
|
|
24
|
+
email
|
|
25
|
+
isGuest
|
|
26
|
+
marketId
|
|
27
|
+
defaultAddress {
|
|
28
|
+
printableAddress
|
|
29
|
+
zipCode
|
|
30
|
+
submarketId
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}`;
|
|
34
|
+
const SEARCH_QUERY = `query searchWithFilterFacetFeed(
|
|
35
|
+
$query: String!
|
|
36
|
+
$cursor: String
|
|
37
|
+
$filterQuery: String
|
|
38
|
+
$isDebug: Boolean
|
|
39
|
+
$fromFilterChange: Boolean
|
|
40
|
+
$serializedBundleGlobalSearchContext: String
|
|
41
|
+
$address: String
|
|
42
|
+
$searchType: String
|
|
43
|
+
) {
|
|
44
|
+
searchWithFilterFacetFeed(
|
|
45
|
+
query: $query
|
|
46
|
+
cursor: $cursor
|
|
47
|
+
filterQuery: $filterQuery
|
|
48
|
+
isDebug: $isDebug
|
|
49
|
+
fromFilterChange: $fromFilterChange
|
|
50
|
+
serializedBundleGlobalSearchContext: $serializedBundleGlobalSearchContext
|
|
51
|
+
address: $address
|
|
52
|
+
searchType: $searchType
|
|
53
|
+
) {
|
|
54
|
+
body {
|
|
55
|
+
id
|
|
56
|
+
header {
|
|
57
|
+
id
|
|
58
|
+
name
|
|
59
|
+
text {
|
|
60
|
+
title
|
|
61
|
+
subtitle
|
|
62
|
+
description
|
|
63
|
+
accessory
|
|
64
|
+
custom {
|
|
65
|
+
key
|
|
66
|
+
value
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
body {
|
|
71
|
+
id
|
|
72
|
+
name
|
|
73
|
+
text {
|
|
74
|
+
title
|
|
75
|
+
subtitle
|
|
76
|
+
description
|
|
77
|
+
accessory
|
|
78
|
+
custom {
|
|
79
|
+
key
|
|
80
|
+
value
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
images {
|
|
84
|
+
main {
|
|
85
|
+
uri
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
events {
|
|
89
|
+
click {
|
|
90
|
+
name
|
|
91
|
+
data
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
childrenMap {
|
|
95
|
+
id
|
|
96
|
+
name
|
|
97
|
+
text {
|
|
98
|
+
title
|
|
99
|
+
subtitle
|
|
100
|
+
description
|
|
101
|
+
accessory
|
|
102
|
+
custom {
|
|
103
|
+
key
|
|
104
|
+
value
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
images {
|
|
108
|
+
main {
|
|
109
|
+
uri
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
events {
|
|
113
|
+
click {
|
|
114
|
+
name
|
|
115
|
+
data
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
component {
|
|
119
|
+
id
|
|
120
|
+
category
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
component {
|
|
124
|
+
id
|
|
125
|
+
category
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}`;
|
|
131
|
+
const STOREPAGE_QUERY = `query storepageFeed(
|
|
132
|
+
$storeId: ID!
|
|
133
|
+
$menuId: ID
|
|
134
|
+
$isMerchantPreview: Boolean
|
|
135
|
+
$fulfillmentType: FulfillmentType
|
|
136
|
+
$cursor: String
|
|
137
|
+
$menuSurfaceArea: MenuSurfaceArea
|
|
138
|
+
$scheduledTime: String
|
|
139
|
+
$scheduledMinTimeUtc: String
|
|
140
|
+
$scheduledMaxTimeUtc: String
|
|
141
|
+
$entryPoint: StoreEntryPoint
|
|
142
|
+
$DMGroups: [DMGroup]
|
|
143
|
+
) {
|
|
144
|
+
storepageFeed(
|
|
145
|
+
storeId: $storeId
|
|
146
|
+
menuId: $menuId
|
|
147
|
+
isMerchantPreview: $isMerchantPreview
|
|
148
|
+
fulfillmentType: $fulfillmentType
|
|
149
|
+
cursor: $cursor
|
|
150
|
+
menuSurfaceArea: $menuSurfaceArea
|
|
151
|
+
scheduledTime: $scheduledTime
|
|
152
|
+
scheduledMinTimeUtc: $scheduledMinTimeUtc
|
|
153
|
+
scheduledMaxTimeUtc: $scheduledMaxTimeUtc
|
|
154
|
+
entryPoint: $entryPoint
|
|
155
|
+
DMGroups: $DMGroups
|
|
156
|
+
) {
|
|
157
|
+
storeHeader {
|
|
158
|
+
id
|
|
159
|
+
name
|
|
160
|
+
description
|
|
161
|
+
business {
|
|
162
|
+
id
|
|
163
|
+
name
|
|
164
|
+
}
|
|
165
|
+
address {
|
|
166
|
+
displayAddress
|
|
167
|
+
}
|
|
168
|
+
ratings {
|
|
169
|
+
averageRating
|
|
170
|
+
numRatingsDisplayString
|
|
171
|
+
}
|
|
172
|
+
coverImgUrl
|
|
173
|
+
}
|
|
174
|
+
menuBook {
|
|
175
|
+
id
|
|
176
|
+
name
|
|
177
|
+
menuCategories {
|
|
178
|
+
id
|
|
179
|
+
name
|
|
180
|
+
numItems
|
|
181
|
+
next {
|
|
182
|
+
anchor
|
|
183
|
+
cursor
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
itemLists {
|
|
188
|
+
id
|
|
189
|
+
name
|
|
190
|
+
description
|
|
191
|
+
items {
|
|
192
|
+
id
|
|
193
|
+
name
|
|
194
|
+
description
|
|
195
|
+
displayPrice
|
|
196
|
+
imageUrl
|
|
197
|
+
nextCursor
|
|
198
|
+
storeId
|
|
199
|
+
}
|
|
200
|
+
itemCategoryTabs {
|
|
201
|
+
id
|
|
202
|
+
name
|
|
203
|
+
items {
|
|
204
|
+
id
|
|
205
|
+
name
|
|
206
|
+
displayPrice
|
|
207
|
+
nextCursor
|
|
208
|
+
storeId
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}`;
|
|
214
|
+
const ITEM_QUERY = `query itemPage(
|
|
215
|
+
$storeId: ID!
|
|
216
|
+
$itemId: ID!
|
|
217
|
+
$consumerId: ID
|
|
218
|
+
$isMerchantPreview: Boolean
|
|
219
|
+
$isNested: Boolean!
|
|
220
|
+
$fulfillmentType: FulfillmentType
|
|
221
|
+
$cursorContext: ItemPageCursorContextInput
|
|
222
|
+
$scheduledMinTimeUtc: String
|
|
223
|
+
$scheduledMaxTimeUtc: String
|
|
224
|
+
) {
|
|
225
|
+
itemPage(
|
|
226
|
+
storeId: $storeId
|
|
227
|
+
itemId: $itemId
|
|
228
|
+
consumerId: $consumerId
|
|
229
|
+
isMerchantPreview: $isMerchantPreview
|
|
230
|
+
fulfillmentType: $fulfillmentType
|
|
231
|
+
cursorContext: $cursorContext
|
|
232
|
+
scheduledMinTimeUtc: $scheduledMinTimeUtc
|
|
233
|
+
scheduledMaxTimeUtc: $scheduledMaxTimeUtc
|
|
234
|
+
) {
|
|
235
|
+
itemHeader @skip(if: $isNested) {
|
|
236
|
+
id
|
|
237
|
+
name
|
|
238
|
+
description
|
|
239
|
+
displayString
|
|
240
|
+
unitAmount
|
|
241
|
+
currency
|
|
242
|
+
decimalPlaces
|
|
243
|
+
menuId
|
|
244
|
+
specialInstructionsMaxLength
|
|
245
|
+
dietaryTagsList {
|
|
246
|
+
type
|
|
247
|
+
abbreviatedTagDisplayString
|
|
248
|
+
fullTagDisplayString
|
|
249
|
+
}
|
|
250
|
+
reviewData {
|
|
251
|
+
ratingDisplayString
|
|
252
|
+
reviewCount
|
|
253
|
+
itemReviewRankingCount
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
optionLists {
|
|
257
|
+
id
|
|
258
|
+
name
|
|
259
|
+
subtitle
|
|
260
|
+
minNumOptions
|
|
261
|
+
maxNumOptions
|
|
262
|
+
numFreeOptions
|
|
263
|
+
isOptional
|
|
264
|
+
options {
|
|
265
|
+
id
|
|
266
|
+
name
|
|
267
|
+
displayString
|
|
268
|
+
unitAmount
|
|
269
|
+
defaultQuantity
|
|
270
|
+
nextCursor
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
itemPreferences {
|
|
274
|
+
id
|
|
275
|
+
title
|
|
276
|
+
specialInstructions {
|
|
277
|
+
title
|
|
278
|
+
characterMaxLength
|
|
279
|
+
isEnabled
|
|
280
|
+
placeholderText
|
|
281
|
+
}
|
|
282
|
+
substitutionPreferences {
|
|
283
|
+
title
|
|
284
|
+
substitutionPreferencesList {
|
|
285
|
+
id
|
|
286
|
+
displayString
|
|
287
|
+
isDefault
|
|
288
|
+
value
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}`;
|
|
294
|
+
const GET_AVAILABLE_ADDRESSES_QUERY = `query getAvailableAddresses {
|
|
295
|
+
getAvailableAddresses {
|
|
296
|
+
id
|
|
297
|
+
addressId
|
|
298
|
+
street
|
|
299
|
+
city
|
|
300
|
+
subpremise
|
|
301
|
+
state
|
|
302
|
+
zipCode
|
|
303
|
+
country
|
|
304
|
+
countryCode
|
|
305
|
+
lat
|
|
306
|
+
lng
|
|
307
|
+
districtId
|
|
308
|
+
manualLat
|
|
309
|
+
manualLng
|
|
310
|
+
timezone
|
|
311
|
+
shortname
|
|
312
|
+
printableAddress
|
|
313
|
+
driverInstructions
|
|
314
|
+
buildingName
|
|
315
|
+
entryCode
|
|
316
|
+
addressLinkType
|
|
317
|
+
formattedAddressSegmentedList
|
|
318
|
+
formattedAddressSegmentedNonUserEditableFieldsList
|
|
319
|
+
personalAddressLabel {
|
|
320
|
+
labelIcon
|
|
321
|
+
labelName
|
|
322
|
+
}
|
|
323
|
+
dropoffPreferences {
|
|
324
|
+
allPreferences {
|
|
325
|
+
optionId
|
|
326
|
+
isDefault
|
|
327
|
+
instructions
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}`;
|
|
332
|
+
const ADD_CONSUMER_ADDRESS_MUTATION = `mutation addConsumerAddressV2(
|
|
333
|
+
$lat: Float!
|
|
334
|
+
$lng: Float!
|
|
335
|
+
$city: String!
|
|
336
|
+
$state: String!
|
|
337
|
+
$zipCode: String!
|
|
338
|
+
$printableAddress: String!
|
|
339
|
+
$shortname: String!
|
|
340
|
+
$googlePlaceId: String!
|
|
341
|
+
$subpremise: String
|
|
342
|
+
$driverInstructions: String
|
|
343
|
+
$dropoffOptionId: String
|
|
344
|
+
$manualLat: Float
|
|
345
|
+
$manualLng: Float
|
|
346
|
+
$addressLinkType: AddressLinkType
|
|
347
|
+
$buildingName: String
|
|
348
|
+
$entryCode: String
|
|
349
|
+
$personalAddressLabel: PersonalAddressLabelInput
|
|
350
|
+
$addressId: String
|
|
351
|
+
) {
|
|
352
|
+
addConsumerAddressV2(
|
|
353
|
+
lat: $lat
|
|
354
|
+
lng: $lng
|
|
355
|
+
city: $city
|
|
356
|
+
state: $state
|
|
357
|
+
zipCode: $zipCode
|
|
358
|
+
printableAddress: $printableAddress
|
|
359
|
+
shortname: $shortname
|
|
360
|
+
googlePlaceId: $googlePlaceId
|
|
361
|
+
subpremise: $subpremise
|
|
362
|
+
driverInstructions: $driverInstructions
|
|
363
|
+
dropoffOptionId: $dropoffOptionId
|
|
364
|
+
manualLat: $manualLat
|
|
365
|
+
manualLng: $manualLng
|
|
366
|
+
addressLinkType: $addressLinkType
|
|
367
|
+
buildingName: $buildingName
|
|
368
|
+
entryCode: $entryCode
|
|
369
|
+
personalAddressLabel: $personalAddressLabel
|
|
370
|
+
addressId: $addressId
|
|
371
|
+
) {
|
|
372
|
+
defaultAddress {
|
|
373
|
+
id
|
|
374
|
+
addressId
|
|
375
|
+
printableAddress
|
|
376
|
+
shortname
|
|
377
|
+
zipCode
|
|
378
|
+
submarketId
|
|
379
|
+
}
|
|
380
|
+
availableAddresses {
|
|
381
|
+
id
|
|
382
|
+
addressId
|
|
383
|
+
printableAddress
|
|
384
|
+
shortname
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}`;
|
|
388
|
+
const UPDATE_CONSUMER_DEFAULT_ADDRESS_MUTATION = `mutation updateConsumerDefaultAddressV2($defaultAddressId: ID!) {
|
|
389
|
+
updateConsumerDefaultAddressV2(defaultAddressId: $defaultAddressId) {
|
|
390
|
+
defaultAddress {
|
|
391
|
+
id
|
|
392
|
+
addressId
|
|
393
|
+
printableAddress
|
|
394
|
+
shortname
|
|
395
|
+
zipCode
|
|
396
|
+
submarketId
|
|
397
|
+
}
|
|
398
|
+
availableAddresses {
|
|
399
|
+
id
|
|
400
|
+
addressId
|
|
401
|
+
printableAddress
|
|
402
|
+
shortname
|
|
403
|
+
}
|
|
404
|
+
orderCart {
|
|
405
|
+
id
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}`;
|
|
409
|
+
const CURRENT_CART_QUERY = `query consumerOrderCart {
|
|
410
|
+
consumerOrderCart {
|
|
411
|
+
id
|
|
412
|
+
subtotal
|
|
413
|
+
total
|
|
414
|
+
currencyCode
|
|
415
|
+
restaurant {
|
|
416
|
+
id
|
|
417
|
+
name
|
|
418
|
+
slug
|
|
419
|
+
business {
|
|
420
|
+
id
|
|
421
|
+
name
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
menu {
|
|
425
|
+
id
|
|
426
|
+
name
|
|
427
|
+
}
|
|
428
|
+
orders {
|
|
429
|
+
id
|
|
430
|
+
orderItems {
|
|
431
|
+
id
|
|
432
|
+
quantity
|
|
433
|
+
specialInstructions
|
|
434
|
+
priceDisplayString
|
|
435
|
+
singlePrice
|
|
436
|
+
priceOfTotalQuantity
|
|
437
|
+
cartItemStatusType
|
|
438
|
+
item {
|
|
439
|
+
id
|
|
440
|
+
name
|
|
441
|
+
storeId
|
|
442
|
+
}
|
|
443
|
+
options {
|
|
444
|
+
id
|
|
445
|
+
name
|
|
446
|
+
quantity
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}`;
|
|
452
|
+
const EXISTING_ORDERS_QUERY = `query getConsumerOrdersWithDetails($offset: Int!, $limit: Int!, $includeCancelled: Boolean) {
|
|
453
|
+
getConsumerOrdersWithDetails(offset: $offset, limit: $limit, includeCancelled: $includeCancelled) {
|
|
454
|
+
id
|
|
455
|
+
orderUuid
|
|
456
|
+
deliveryUuid
|
|
457
|
+
createdAt
|
|
458
|
+
submittedAt
|
|
459
|
+
cancelledAt
|
|
460
|
+
fulfilledAt
|
|
461
|
+
specialInstructions
|
|
462
|
+
isReorderable
|
|
463
|
+
isGift
|
|
464
|
+
isPickup
|
|
465
|
+
isRetail
|
|
466
|
+
isMerchantShipping
|
|
467
|
+
containsAlcohol
|
|
468
|
+
fulfillmentType
|
|
469
|
+
shoppingProtocol
|
|
470
|
+
orderFilterType
|
|
471
|
+
pollingInterval
|
|
472
|
+
creator {
|
|
473
|
+
id
|
|
474
|
+
firstName
|
|
475
|
+
lastName
|
|
476
|
+
}
|
|
477
|
+
deliveryAddress {
|
|
478
|
+
id
|
|
479
|
+
formattedAddress
|
|
480
|
+
}
|
|
481
|
+
orders {
|
|
482
|
+
id
|
|
483
|
+
items {
|
|
484
|
+
id
|
|
485
|
+
name
|
|
486
|
+
quantity
|
|
487
|
+
specialInstructions
|
|
488
|
+
substitutionPreferences
|
|
489
|
+
originalItemPrice
|
|
490
|
+
purchaseType
|
|
491
|
+
purchaseQuantity {
|
|
492
|
+
continuousQuantity {
|
|
493
|
+
quantity
|
|
494
|
+
unit
|
|
495
|
+
}
|
|
496
|
+
discreteQuantity {
|
|
497
|
+
quantity
|
|
498
|
+
unit
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
fulfillQuantity {
|
|
502
|
+
continuousQuantity {
|
|
503
|
+
quantity
|
|
504
|
+
unit
|
|
505
|
+
}
|
|
506
|
+
discreteQuantity {
|
|
507
|
+
quantity
|
|
508
|
+
unit
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
orderItemExtras {
|
|
512
|
+
menuItemExtraId
|
|
513
|
+
name
|
|
514
|
+
orderItemExtraOptions {
|
|
515
|
+
menuExtraOptionId
|
|
516
|
+
name
|
|
517
|
+
description
|
|
518
|
+
price
|
|
519
|
+
quantity
|
|
520
|
+
orderItemExtras {
|
|
521
|
+
menuItemExtraId
|
|
522
|
+
name
|
|
523
|
+
orderItemExtraOptions {
|
|
524
|
+
menuExtraOptionId
|
|
525
|
+
name
|
|
526
|
+
description
|
|
527
|
+
price
|
|
528
|
+
quantity
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
grandTotal {
|
|
536
|
+
unitAmount
|
|
537
|
+
currency
|
|
538
|
+
decimalPlaces
|
|
539
|
+
displayString
|
|
540
|
+
sign
|
|
541
|
+
}
|
|
542
|
+
likelyOosItems {
|
|
543
|
+
menuItemId
|
|
544
|
+
name
|
|
545
|
+
photoUrl
|
|
546
|
+
}
|
|
547
|
+
store {
|
|
548
|
+
id
|
|
549
|
+
name
|
|
550
|
+
business {
|
|
551
|
+
id
|
|
552
|
+
name
|
|
553
|
+
}
|
|
554
|
+
phoneNumber
|
|
555
|
+
fulfillsOwnDeliveries
|
|
556
|
+
customerArrivedPickupInstructions
|
|
557
|
+
rerouteStoreId
|
|
558
|
+
}
|
|
559
|
+
recurringOrderDetails {
|
|
560
|
+
itemNames
|
|
561
|
+
consumerId
|
|
562
|
+
recurringOrderUpcomingOrderUuid
|
|
563
|
+
scheduledDeliveryDate
|
|
564
|
+
arrivalTimeDisplayString
|
|
565
|
+
storeName
|
|
566
|
+
isCancelled
|
|
567
|
+
}
|
|
568
|
+
bundleOrderInfo {
|
|
569
|
+
primaryBundleOrderUuid
|
|
570
|
+
primaryBundleOrderId
|
|
571
|
+
bundleOrderUuids
|
|
572
|
+
bundleOrderConfig {
|
|
573
|
+
bundleType
|
|
574
|
+
bundleOrderRole
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
cancellationPendingRefundInfo {
|
|
578
|
+
state
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}`;
|
|
582
|
+
const ADD_TO_CART_MUTATION = `mutation addCartItem(
|
|
583
|
+
$addCartItemInput: AddCartItemInput!
|
|
584
|
+
$fulfillmentContext: FulfillmentContextInput!
|
|
585
|
+
$cartContext: CartContextInput
|
|
586
|
+
$returnCartFromOrderService: Boolean
|
|
587
|
+
$monitoringContext: MonitoringContextInput
|
|
588
|
+
$lowPriorityBatchAddCartItemInput: [AddCartItemInput!]
|
|
589
|
+
$shouldKeepOnlyOneActiveCart: Boolean
|
|
590
|
+
$selectedDeliveryOption: SelectedDeliveryOptionInput
|
|
591
|
+
) {
|
|
592
|
+
addCartItemV2(
|
|
593
|
+
addCartItemInput: $addCartItemInput
|
|
594
|
+
fulfillmentContext: $fulfillmentContext
|
|
595
|
+
cartContext: $cartContext
|
|
596
|
+
returnCartFromOrderService: $returnCartFromOrderService
|
|
597
|
+
monitoringContext: $monitoringContext
|
|
598
|
+
lowPriorityBatchAddCartItemInput: $lowPriorityBatchAddCartItemInput
|
|
599
|
+
shouldKeepOnlyOneActiveCart: $shouldKeepOnlyOneActiveCart
|
|
600
|
+
selectedDeliveryOption: $selectedDeliveryOption
|
|
601
|
+
) {
|
|
602
|
+
id
|
|
603
|
+
subtotal
|
|
604
|
+
total
|
|
605
|
+
currencyCode
|
|
606
|
+
restaurant {
|
|
607
|
+
id
|
|
608
|
+
name
|
|
609
|
+
slug
|
|
610
|
+
business {
|
|
611
|
+
id
|
|
612
|
+
name
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
menu {
|
|
616
|
+
id
|
|
617
|
+
name
|
|
618
|
+
}
|
|
619
|
+
orders {
|
|
620
|
+
id
|
|
621
|
+
orderItems {
|
|
622
|
+
id
|
|
623
|
+
quantity
|
|
624
|
+
specialInstructions
|
|
625
|
+
priceDisplayString
|
|
626
|
+
singlePrice
|
|
627
|
+
priceOfTotalQuantity
|
|
628
|
+
item {
|
|
629
|
+
id
|
|
630
|
+
name
|
|
631
|
+
storeId
|
|
632
|
+
}
|
|
633
|
+
options {
|
|
634
|
+
id
|
|
635
|
+
name
|
|
636
|
+
quantity
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}`;
|
|
642
|
+
const UPDATE_CART_MUTATION = `mutation updateCartItem(
|
|
643
|
+
$updateCartItemApiParams: UpdateCartItemInput!
|
|
644
|
+
$fulfillmentContext: FulfillmentContextInput!
|
|
645
|
+
$returnCartFromOrderService: Boolean
|
|
646
|
+
$shouldKeepOnlyOneActiveCart: Boolean
|
|
647
|
+
$cartContextFilter: CartContextV2
|
|
648
|
+
) {
|
|
649
|
+
updateCartItemV2(
|
|
650
|
+
updateCartItemInput: $updateCartItemApiParams
|
|
651
|
+
fulfillmentContext: $fulfillmentContext
|
|
652
|
+
returnCartFromOrderService: $returnCartFromOrderService
|
|
653
|
+
shouldKeepOnlyOneActiveCart: $shouldKeepOnlyOneActiveCart
|
|
654
|
+
cartContextFilter: $cartContextFilter
|
|
655
|
+
) {
|
|
656
|
+
id
|
|
657
|
+
subtotal
|
|
658
|
+
total
|
|
659
|
+
currencyCode
|
|
660
|
+
restaurant {
|
|
661
|
+
id
|
|
662
|
+
name
|
|
663
|
+
slug
|
|
664
|
+
business {
|
|
665
|
+
id
|
|
666
|
+
name
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
menu {
|
|
670
|
+
id
|
|
671
|
+
name
|
|
672
|
+
}
|
|
673
|
+
orders {
|
|
674
|
+
id
|
|
675
|
+
orderItems {
|
|
676
|
+
id
|
|
677
|
+
quantity
|
|
678
|
+
specialInstructions
|
|
679
|
+
priceDisplayString
|
|
680
|
+
singlePrice
|
|
681
|
+
priceOfTotalQuantity
|
|
682
|
+
item {
|
|
683
|
+
id
|
|
684
|
+
name
|
|
685
|
+
storeId
|
|
686
|
+
}
|
|
687
|
+
options {
|
|
688
|
+
id
|
|
689
|
+
name
|
|
690
|
+
quantity
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}`;
|
|
696
|
+
class DoorDashDirectSession {
|
|
697
|
+
browser = null;
|
|
698
|
+
context = null;
|
|
699
|
+
page = null;
|
|
700
|
+
attemptedManagedImport = false;
|
|
701
|
+
async init(options = {}) {
|
|
702
|
+
if (this.page) {
|
|
703
|
+
return this.page;
|
|
704
|
+
}
|
|
705
|
+
await this.maybeImportManagedBrowserSession();
|
|
706
|
+
const storageStatePath = getStorageStatePath();
|
|
707
|
+
this.browser = await chromium.launch({
|
|
708
|
+
headless: options.headed ? false : true,
|
|
709
|
+
args: ["--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-setuid-sandbox"],
|
|
710
|
+
});
|
|
711
|
+
this.context = await this.browser.newContext({
|
|
712
|
+
userAgent: DEFAULT_USER_AGENT,
|
|
713
|
+
locale: "en-US",
|
|
714
|
+
viewport: { width: 1280, height: 900 },
|
|
715
|
+
...(await hasStorageState()) ? { storageState: storageStatePath } : {},
|
|
716
|
+
});
|
|
717
|
+
if (!(await hasStorageState())) {
|
|
718
|
+
const cookies = await readStoredCookies();
|
|
719
|
+
if (cookies.length > 0) {
|
|
720
|
+
await this.context.addCookies(cookies);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
this.page = await this.context.newPage();
|
|
724
|
+
await this.page.goto(`${BASE_URL}/`, { waitUntil: "domcontentloaded", timeout: 90_000 });
|
|
725
|
+
await this.page.waitForTimeout(1_500);
|
|
726
|
+
return this.page;
|
|
727
|
+
}
|
|
728
|
+
async graphql(operationName, query, variables) {
|
|
729
|
+
const raw = await this.requestRaw({
|
|
730
|
+
url: `${BASE_URL}/graphql/${operationName}?operation=${operationName}`,
|
|
731
|
+
method: "POST",
|
|
732
|
+
headers: GRAPHQL_HEADERS,
|
|
733
|
+
body: JSON.stringify({ operationName, variables, query }),
|
|
734
|
+
});
|
|
735
|
+
return parseGraphQlResponse(operationName, raw.status, raw.text);
|
|
736
|
+
}
|
|
737
|
+
async requestJson(input) {
|
|
738
|
+
const raw = await this.requestRaw(input);
|
|
739
|
+
const parsed = safeJsonParse(raw.text);
|
|
740
|
+
if (!parsed) {
|
|
741
|
+
const label = input.operationName ?? input.url;
|
|
742
|
+
throw new Error(`DoorDash ${label} returned HTTP ${raw.status} with a non-JSON response. Response snippet: ${truncate(raw.text, 240)}`);
|
|
743
|
+
}
|
|
744
|
+
return parsed;
|
|
745
|
+
}
|
|
746
|
+
async ordersPageSnapshot() {
|
|
747
|
+
const page = await this.init();
|
|
748
|
+
await page.goto(`${BASE_URL}/orders/`, { waitUntil: "domcontentloaded", timeout: 90_000 });
|
|
749
|
+
await page.waitForTimeout(3_000);
|
|
750
|
+
return page.evaluate(() => {
|
|
751
|
+
const globalWindow = window;
|
|
752
|
+
const bodyText = document.body?.innerText ?? "";
|
|
753
|
+
return {
|
|
754
|
+
cache: globalWindow.__APOLLO_CLIENT__?.cache?.extract?.() ?? null,
|
|
755
|
+
noOrdersBanner: /No orders yet/i.test(bodyText),
|
|
756
|
+
turnstileOverlayVisible: Boolean(document.querySelector('[data-testid="turnstile/overlay"]')),
|
|
757
|
+
url: window.location.href,
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
async saveState() {
|
|
762
|
+
if (!this.context) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
await saveContextState(this.context);
|
|
766
|
+
}
|
|
767
|
+
async close() {
|
|
768
|
+
await this.page?.close().catch(() => { });
|
|
769
|
+
await this.context?.close().catch(() => { });
|
|
770
|
+
await this.browser?.close().catch(() => { });
|
|
771
|
+
this.page = null;
|
|
772
|
+
this.context = null;
|
|
773
|
+
this.browser = null;
|
|
774
|
+
}
|
|
775
|
+
async requestRaw(input) {
|
|
776
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
777
|
+
const page = await this.init();
|
|
778
|
+
try {
|
|
779
|
+
return await page.evaluate(async ({ targetUrl, method, headers, body }) => {
|
|
780
|
+
const response = await fetch(targetUrl, {
|
|
781
|
+
method,
|
|
782
|
+
headers,
|
|
783
|
+
body,
|
|
784
|
+
});
|
|
785
|
+
return {
|
|
786
|
+
status: response.status,
|
|
787
|
+
text: await response.text(),
|
|
788
|
+
};
|
|
789
|
+
}, {
|
|
790
|
+
targetUrl: input.url,
|
|
791
|
+
method: input.method ?? "GET",
|
|
792
|
+
headers: input.headers ?? {},
|
|
793
|
+
body: input.body,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
if (attempt === 1 || !isRetryablePageEvaluateError(error)) {
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
800
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 10_000 }).catch(() => { });
|
|
801
|
+
await page.waitForTimeout(500).catch(() => { });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
throw new Error(`DoorDash request failed for ${input.url}`);
|
|
805
|
+
}
|
|
806
|
+
async maybeImportManagedBrowserSession() {
|
|
807
|
+
if (this.attemptedManagedImport) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
this.attemptedManagedImport = true;
|
|
811
|
+
await importManagedBrowserSessionIfAvailable().catch(() => { });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const session = new DoorDashDirectSession();
|
|
815
|
+
export async function checkAuthDirect() {
|
|
816
|
+
const data = await session.graphql("consumer", CONSUMER_QUERY, {});
|
|
817
|
+
const consumer = data.consumer ?? null;
|
|
818
|
+
return {
|
|
819
|
+
success: true,
|
|
820
|
+
isLoggedIn: Boolean(consumer && consumer.isGuest === false),
|
|
821
|
+
email: consumer?.email ?? null,
|
|
822
|
+
firstName: consumer?.firstName ?? null,
|
|
823
|
+
lastName: consumer?.lastName ?? null,
|
|
824
|
+
consumerId: consumer?.id ?? null,
|
|
825
|
+
marketId: consumer?.marketId ?? null,
|
|
826
|
+
defaultAddress: consumer?.defaultAddress
|
|
827
|
+
? {
|
|
828
|
+
printableAddress: consumer.defaultAddress.printableAddress ?? null,
|
|
829
|
+
zipCode: consumer.defaultAddress.zipCode ?? null,
|
|
830
|
+
submarketId: consumer.defaultAddress.submarketId ?? null,
|
|
831
|
+
}
|
|
832
|
+
: null,
|
|
833
|
+
cookiesPath: getCookiesPath(),
|
|
834
|
+
storageStatePath: getStorageStatePath(),
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
export async function bootstrapAuthSession() {
|
|
838
|
+
const page = await session.init({ headed: true });
|
|
839
|
+
console.error("A Chromium window is open for DoorDash session bootstrap.");
|
|
840
|
+
console.error("1) Sign in if needed.");
|
|
841
|
+
console.error("2) Confirm your delivery address if needed.");
|
|
842
|
+
console.error("3) Return here and press Enter to save the session for direct API use.");
|
|
843
|
+
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
844
|
+
const rl = createInterface({ input, output });
|
|
845
|
+
try {
|
|
846
|
+
await rl.question("");
|
|
847
|
+
}
|
|
848
|
+
finally {
|
|
849
|
+
rl.close();
|
|
850
|
+
}
|
|
851
|
+
await session.saveState();
|
|
852
|
+
const auth = await checkAuthDirect();
|
|
853
|
+
return {
|
|
854
|
+
...auth,
|
|
855
|
+
message: auth.isLoggedIn
|
|
856
|
+
? "DoorDash session saved for direct API use."
|
|
857
|
+
: "DoorDash session state saved, but the consumer still appears to be logged out or guest-only.",
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
export async function clearStoredSession() {
|
|
861
|
+
await session.close();
|
|
862
|
+
await rm(getCookiesPath(), { force: true }).catch(() => { });
|
|
863
|
+
await rm(getStorageStatePath(), { force: true }).catch(() => { });
|
|
864
|
+
return {
|
|
865
|
+
success: true,
|
|
866
|
+
message: "DoorDash cookies and stored browser session state cleared.",
|
|
867
|
+
cookiesPath: getCookiesPath(),
|
|
868
|
+
storageStatePath: getStorageStatePath(),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
export async function setAddressDirect(address) {
|
|
872
|
+
const requestedAddress = address.trim();
|
|
873
|
+
if (!requestedAddress) {
|
|
874
|
+
throw new Error("Missing required address text.");
|
|
875
|
+
}
|
|
876
|
+
const availableAddresses = await getAvailableAddressesDirect();
|
|
877
|
+
const directMatch = resolveAvailableAddressMatch({
|
|
878
|
+
input: requestedAddress,
|
|
879
|
+
availableAddresses,
|
|
880
|
+
});
|
|
881
|
+
if (directMatch) {
|
|
882
|
+
return updateConsumerDefaultAddressDirect(requestedAddress, directMatch);
|
|
883
|
+
}
|
|
884
|
+
const autocomplete = await autocompleteAddressDirect(requestedAddress);
|
|
885
|
+
const prediction = autocomplete[0];
|
|
886
|
+
if (!prediction) {
|
|
887
|
+
throw new Error(`DoorDash returned no address predictions for "${requestedAddress}".`);
|
|
888
|
+
}
|
|
889
|
+
const createdAddress = await getOrCreateAddressDirect(prediction);
|
|
890
|
+
const autocompleteMatch = resolveAvailableAddressMatch({
|
|
891
|
+
input: requestedAddress,
|
|
892
|
+
availableAddresses,
|
|
893
|
+
prediction,
|
|
894
|
+
createdAddress,
|
|
895
|
+
});
|
|
896
|
+
if (autocompleteMatch) {
|
|
897
|
+
return updateConsumerDefaultAddressDirect(requestedAddress, autocompleteMatch);
|
|
898
|
+
}
|
|
899
|
+
const enrollmentPayload = buildAddConsumerAddressPayload({
|
|
900
|
+
requestedAddress,
|
|
901
|
+
prediction,
|
|
902
|
+
createdAddress,
|
|
903
|
+
});
|
|
904
|
+
return addConsumerAddressDirect(requestedAddress, enrollmentPayload);
|
|
905
|
+
}
|
|
906
|
+
export async function searchRestaurantsDirect(query, cuisine) {
|
|
907
|
+
const data = await session.graphql("searchWithFilterFacetFeed", SEARCH_QUERY, {
|
|
908
|
+
query,
|
|
909
|
+
cursor: "",
|
|
910
|
+
filterQuery: "",
|
|
911
|
+
isDebug: false,
|
|
912
|
+
searchType: "",
|
|
913
|
+
});
|
|
914
|
+
const rows = parseSearchRestaurants(data.searchWithFilterFacetFeed?.body ?? []);
|
|
915
|
+
const cuisineFilter = cuisine?.trim() ? cuisine.trim() : null;
|
|
916
|
+
const restaurants = cuisineFilter
|
|
917
|
+
? rows.filter((row) => row.description?.toLowerCase().includes(cuisineFilter.toLowerCase()))
|
|
918
|
+
: rows;
|
|
919
|
+
return {
|
|
920
|
+
success: true,
|
|
921
|
+
query,
|
|
922
|
+
cuisineFilter,
|
|
923
|
+
count: restaurants.length,
|
|
924
|
+
restaurants,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
export async function getMenuDirect(restaurantId) {
|
|
928
|
+
const data = await session.graphql("storepageFeed", STOREPAGE_QUERY, {
|
|
929
|
+
storeId: restaurantId,
|
|
930
|
+
menuId: null,
|
|
931
|
+
isMerchantPreview: false,
|
|
932
|
+
fulfillmentType: "Delivery",
|
|
933
|
+
cursor: null,
|
|
934
|
+
scheduledTime: null,
|
|
935
|
+
entryPoint: "External",
|
|
936
|
+
});
|
|
937
|
+
return parseMenuResponse(data.storepageFeed, restaurantId);
|
|
938
|
+
}
|
|
939
|
+
export async function getItemDirect(restaurantId, itemId) {
|
|
940
|
+
const auth = await checkAuthDirect();
|
|
941
|
+
const data = await session.graphql("itemPage", ITEM_QUERY, {
|
|
942
|
+
storeId: restaurantId,
|
|
943
|
+
itemId,
|
|
944
|
+
consumerId: auth.consumerId,
|
|
945
|
+
isMerchantPreview: false,
|
|
946
|
+
isNested: false,
|
|
947
|
+
fulfillmentType: "Delivery",
|
|
948
|
+
cursorContext: null,
|
|
949
|
+
});
|
|
950
|
+
return parseItemResponse(data.itemPage, restaurantId);
|
|
951
|
+
}
|
|
952
|
+
export async function getCartDirect() {
|
|
953
|
+
const data = await session.graphql("consumerOrderCart", CURRENT_CART_QUERY, {});
|
|
954
|
+
return parseCartResponse(data.consumerOrderCart ?? null);
|
|
955
|
+
}
|
|
956
|
+
export async function getOrdersDirect(params = {}) {
|
|
957
|
+
const requestedLimit = params.limit;
|
|
958
|
+
const activeOnly = params.activeOnly ?? false;
|
|
959
|
+
try {
|
|
960
|
+
const orders = await fetchExistingOrdersGraphql({ limit: requestedLimit });
|
|
961
|
+
return buildOrdersResult({
|
|
962
|
+
source: "graphql",
|
|
963
|
+
warning: null,
|
|
964
|
+
orders,
|
|
965
|
+
activeOnly,
|
|
966
|
+
requestedLimit,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
if (!isOrderHistoryChallengeError(error)) {
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
973
|
+
const snapshot = await session.ordersPageSnapshot();
|
|
974
|
+
const orders = extractExistingOrdersFromApolloCache(snapshot.cache);
|
|
975
|
+
const warningParts = [
|
|
976
|
+
"DoorDash challenged the direct order-history GraphQL request, so this response was recovered from the consumer-web orders page cache.",
|
|
977
|
+
"This fallback is read-only and can be temporarily empty or limited to the first cached page.",
|
|
978
|
+
];
|
|
979
|
+
if (snapshot.turnstileOverlayVisible) {
|
|
980
|
+
warningParts.push("The orders page was still showing DoorDash's security check banner while this snapshot was captured.");
|
|
981
|
+
}
|
|
982
|
+
if (snapshot.noOrdersBanner) {
|
|
983
|
+
warningParts.push("The live orders page rendered its 'No orders yet' state for this session.");
|
|
984
|
+
}
|
|
985
|
+
return buildOrdersResult({
|
|
986
|
+
source: "orders-page-cache",
|
|
987
|
+
warning: warningParts.join(" "),
|
|
988
|
+
orders,
|
|
989
|
+
activeOnly,
|
|
990
|
+
requestedLimit,
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
export async function getOrderDirect(orderId) {
|
|
995
|
+
const requestedOrderId = orderId.trim();
|
|
996
|
+
if (!requestedOrderId) {
|
|
997
|
+
throw new Error("Missing required order identifier.");
|
|
998
|
+
}
|
|
999
|
+
const orders = await getOrdersDirect();
|
|
1000
|
+
const match = findExistingOrderByIdentifier(orders.orders, requestedOrderId);
|
|
1001
|
+
if (!match) {
|
|
1002
|
+
throw new Error(`Could not find order ${requestedOrderId} in the available existing-order history.`);
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
success: true,
|
|
1006
|
+
source: orders.source,
|
|
1007
|
+
warning: orders.warning,
|
|
1008
|
+
matchedField: match.matchedField,
|
|
1009
|
+
order: match.order,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
export async function addToCartDirect(params) {
|
|
1013
|
+
const { item, itemDetail } = await resolveMenuItem(params.restaurantId, params.itemId, params.itemName);
|
|
1014
|
+
const currentCart = await getCartDirect();
|
|
1015
|
+
const currentCartStoreId = currentCart.restaurant?.id ?? null;
|
|
1016
|
+
const cartId = currentCartStoreId && currentCartStoreId !== params.restaurantId ? "" : (currentCart.cartId ?? "");
|
|
1017
|
+
const auth = await checkAuthDirect();
|
|
1018
|
+
const payload = await buildAddToCartPayload({
|
|
1019
|
+
restaurantId: params.restaurantId,
|
|
1020
|
+
cartId,
|
|
1021
|
+
quantity: params.quantity,
|
|
1022
|
+
specialInstructions: params.specialInstructions ?? null,
|
|
1023
|
+
optionSelections: params.optionSelections ?? [],
|
|
1024
|
+
item,
|
|
1025
|
+
itemDetail,
|
|
1026
|
+
consumerId: auth.consumerId,
|
|
1027
|
+
});
|
|
1028
|
+
const data = await session.graphql("addCartItem", ADD_TO_CART_MUTATION, payload);
|
|
1029
|
+
const cart = parseCartResponse(data.addCartItemV2 ?? null);
|
|
1030
|
+
await session.saveState();
|
|
1031
|
+
return {
|
|
1032
|
+
...cart,
|
|
1033
|
+
sourceItem: {
|
|
1034
|
+
id: item.id,
|
|
1035
|
+
name: item.name,
|
|
1036
|
+
menuId: itemDetail.item.menuId,
|
|
1037
|
+
},
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
export async function updateCartDirect(params) {
|
|
1041
|
+
const currentCart = await getCartDirect();
|
|
1042
|
+
if (!currentCart.cartId || !currentCart.restaurant?.id) {
|
|
1043
|
+
throw new Error("No active cart found to update.");
|
|
1044
|
+
}
|
|
1045
|
+
const cartItem = currentCart.items.find((item) => item.cartItemId === params.cartItemId);
|
|
1046
|
+
if (!cartItem?.itemId) {
|
|
1047
|
+
throw new Error(`Could not find cart item ${params.cartItemId} in the active cart.`);
|
|
1048
|
+
}
|
|
1049
|
+
const payload = buildUpdateCartPayload({
|
|
1050
|
+
cartId: currentCart.cartId,
|
|
1051
|
+
cartItemId: cartItem.cartItemId,
|
|
1052
|
+
itemId: cartItem.itemId,
|
|
1053
|
+
quantity: params.quantity,
|
|
1054
|
+
storeId: currentCart.restaurant.id,
|
|
1055
|
+
});
|
|
1056
|
+
const data = await session.graphql("updateCartItem", UPDATE_CART_MUTATION, payload);
|
|
1057
|
+
const cart = parseCartResponse(data.updateCartItemV2 ?? null);
|
|
1058
|
+
await session.saveState();
|
|
1059
|
+
return {
|
|
1060
|
+
...cart,
|
|
1061
|
+
updatedCartItemId: params.cartItemId,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
export async function cleanupDirect() {
|
|
1065
|
+
await session.close();
|
|
1066
|
+
}
|
|
1067
|
+
export function normalizeItemName(value) {
|
|
1068
|
+
return value.trim().replace(/\s+/g, " ").toLowerCase();
|
|
1069
|
+
}
|
|
1070
|
+
export async function buildAddToCartPayload(input) {
|
|
1071
|
+
const header = input.itemDetail.item;
|
|
1072
|
+
if (!header.id || !header.name || !header.menuId || header.unitAmount == null || !header.currency) {
|
|
1073
|
+
throw new Error("DoorDash item details were incomplete; cannot build a cart mutation safely.");
|
|
1074
|
+
}
|
|
1075
|
+
if (!Number.isInteger(input.quantity) || input.quantity < 1) {
|
|
1076
|
+
throw new Error(`Invalid quantity: ${input.quantity}`);
|
|
1077
|
+
}
|
|
1078
|
+
const resolveNestedOptionLists = input.resolveNestedOptionLists ??
|
|
1079
|
+
(async ({ restaurantId, consumerId, option }) => fetchNestedOptionListsDirect({ restaurantId, consumerId, option }));
|
|
1080
|
+
const builtOptions = await buildNestedOptionsPayload({
|
|
1081
|
+
restaurantId: input.restaurantId,
|
|
1082
|
+
menuId: header.menuId,
|
|
1083
|
+
currency: header.currency,
|
|
1084
|
+
consumerId: input.consumerId ?? null,
|
|
1085
|
+
optionLists: input.itemDetail.item.optionLists,
|
|
1086
|
+
selections: input.optionSelections ?? [],
|
|
1087
|
+
mode: "regular",
|
|
1088
|
+
resolveNestedOptionLists,
|
|
1089
|
+
});
|
|
1090
|
+
return {
|
|
1091
|
+
addCartItemInput: {
|
|
1092
|
+
storeId: input.restaurantId,
|
|
1093
|
+
menuId: header.menuId,
|
|
1094
|
+
itemId: header.id,
|
|
1095
|
+
itemName: header.name,
|
|
1096
|
+
itemDescription: header.description,
|
|
1097
|
+
currency: header.currency,
|
|
1098
|
+
quantity: input.quantity,
|
|
1099
|
+
nestedOptions: JSON.stringify(builtOptions.nestedOptions),
|
|
1100
|
+
specialInstructions: input.specialInstructions,
|
|
1101
|
+
substitutionPreference: "substitute",
|
|
1102
|
+
isBundle: false,
|
|
1103
|
+
bundleType: "BUNDLE_TYPE_UNSPECIFIED",
|
|
1104
|
+
unitPrice: header.unitAmount,
|
|
1105
|
+
cartId: input.cartId,
|
|
1106
|
+
},
|
|
1107
|
+
lowPriorityBatchAddCartItemInput: builtOptions.lowPriorityItems.map((item) => ({
|
|
1108
|
+
cartId: input.cartId,
|
|
1109
|
+
storeId: item.storeId,
|
|
1110
|
+
menuId: item.menuId,
|
|
1111
|
+
itemId: item.itemId,
|
|
1112
|
+
itemName: item.itemName,
|
|
1113
|
+
currency: item.currency,
|
|
1114
|
+
quantity: item.quantity,
|
|
1115
|
+
unitPrice: item.unitPrice,
|
|
1116
|
+
isBundle: false,
|
|
1117
|
+
bundleType: "BUNDLE_TYPE_UNSPECIFIED",
|
|
1118
|
+
nestedOptions: JSON.stringify(item.nestedOptions),
|
|
1119
|
+
})),
|
|
1120
|
+
fulfillmentContext: {
|
|
1121
|
+
shouldUpdateFulfillment: false,
|
|
1122
|
+
fulfillmentType: "Delivery",
|
|
1123
|
+
},
|
|
1124
|
+
monitoringContext: {
|
|
1125
|
+
isGroup: false,
|
|
1126
|
+
},
|
|
1127
|
+
cartContext: {
|
|
1128
|
+
isBundle: false,
|
|
1129
|
+
},
|
|
1130
|
+
returnCartFromOrderService: false,
|
|
1131
|
+
shouldKeepOnlyOneActiveCart: false,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
export function buildUpdateCartPayload(input) {
|
|
1135
|
+
if (!Number.isInteger(input.quantity) || input.quantity < 0) {
|
|
1136
|
+
throw new Error(`Invalid quantity: ${input.quantity}`);
|
|
1137
|
+
}
|
|
1138
|
+
return {
|
|
1139
|
+
updateCartItemApiParams: {
|
|
1140
|
+
cartId: input.cartId,
|
|
1141
|
+
cartItemId: input.cartItemId,
|
|
1142
|
+
itemId: input.itemId,
|
|
1143
|
+
quantity: input.quantity,
|
|
1144
|
+
storeId: input.storeId,
|
|
1145
|
+
purchaseTypeOptions: {
|
|
1146
|
+
purchaseType: "PURCHASE_TYPE_UNSPECIFIED",
|
|
1147
|
+
continuousQuantity: 0,
|
|
1148
|
+
unit: null,
|
|
1149
|
+
},
|
|
1150
|
+
cartFilter: null,
|
|
1151
|
+
},
|
|
1152
|
+
fulfillmentContext: {
|
|
1153
|
+
shouldUpdateFulfillment: false,
|
|
1154
|
+
},
|
|
1155
|
+
returnCartFromOrderService: false,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
export function parseOptionSelectionsJson(value) {
|
|
1159
|
+
const parsed = safeJsonParse(value);
|
|
1160
|
+
if (!Array.isArray(parsed)) {
|
|
1161
|
+
throw new Error("--options-json must be a JSON array of { groupId, optionId, quantity?, children? } objects.");
|
|
1162
|
+
}
|
|
1163
|
+
return parsed.map((entry, index) => parseRequestedOptionSelection(entry, `index ${index}`));
|
|
1164
|
+
}
|
|
1165
|
+
async function buildNestedOptionsPayload(input) {
|
|
1166
|
+
const selections = normalizeRequestedOptionSelections(input.selections);
|
|
1167
|
+
const requiredGroups = input.optionLists.filter((group) => group.minNumOptions > 0 && !group.isOptional);
|
|
1168
|
+
if (selections.length === 0) {
|
|
1169
|
+
if (requiredGroups.length === 0) {
|
|
1170
|
+
return { nestedOptions: [], lowPriorityItems: [] };
|
|
1171
|
+
}
|
|
1172
|
+
const labels = requiredGroups.map((group) => `${group.name} (${group.minNumOptions}-${group.maxNumOptions})`);
|
|
1173
|
+
throw new Error(`This item has required option groups. Provide --options-json with validated groupId/optionId selections. Required groups: ${labels.join(", ")}`);
|
|
1174
|
+
}
|
|
1175
|
+
const groupsById = new Map(input.optionLists.map((group) => [group.id, group]));
|
|
1176
|
+
const selectionsByGroup = new Map();
|
|
1177
|
+
for (const selection of selections) {
|
|
1178
|
+
const group = groupsById.get(selection.groupId);
|
|
1179
|
+
if (!group) {
|
|
1180
|
+
throw new Error(`Unknown option group: ${selection.groupId}`);
|
|
1181
|
+
}
|
|
1182
|
+
const option = group.options.find((candidate) => candidate.id === selection.optionId);
|
|
1183
|
+
if (!option) {
|
|
1184
|
+
throw new Error(`Unknown option ${selection.optionId} for group ${group.name} (${group.id}).`);
|
|
1185
|
+
}
|
|
1186
|
+
const entries = selectionsByGroup.get(group.id) ?? [];
|
|
1187
|
+
entries.push({ selection, option, quantity: selection.quantity ?? 1, group });
|
|
1188
|
+
selectionsByGroup.set(group.id, entries);
|
|
1189
|
+
}
|
|
1190
|
+
validateSelectedOptionCounts(input.optionLists, selectionsByGroup);
|
|
1191
|
+
const nestedOptions = [];
|
|
1192
|
+
const lowPriorityItems = [];
|
|
1193
|
+
for (const group of input.optionLists) {
|
|
1194
|
+
const selectedEntries = selectionsByGroup.get(group.id) ?? [];
|
|
1195
|
+
for (const { selection, option, quantity } of selectedEntries) {
|
|
1196
|
+
if (!option.nextCursor) {
|
|
1197
|
+
if (selection.children && selection.children.length > 0) {
|
|
1198
|
+
throw new Error(`Option ${option.name} (${option.id}) does not open a nested configuration step, so child selections are invalid.`);
|
|
1199
|
+
}
|
|
1200
|
+
nestedOptions.push(input.mode === "standalone-child"
|
|
1201
|
+
? buildStandaloneChildLeafOption({ option, quantity })
|
|
1202
|
+
: buildRegularLeafOption({ option, quantity, group }));
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (!isStandaloneRecommendedGroup(group)) {
|
|
1206
|
+
throw new Error(`Option ${option.name} (${option.id}) opens an additional nested configuration step, but DoorDash's safe direct cart shape is only confirmed for standalone recommended add-on groups (recommended_option_*). Group ${group.id} does not match that transport, so the CLI refuses to guess.`);
|
|
1207
|
+
}
|
|
1208
|
+
const childOptionLists = await input.resolveNestedOptionLists({
|
|
1209
|
+
restaurantId: input.restaurantId,
|
|
1210
|
+
consumerId: input.consumerId,
|
|
1211
|
+
option,
|
|
1212
|
+
group,
|
|
1213
|
+
selection,
|
|
1214
|
+
});
|
|
1215
|
+
const childPayload = await buildNestedOptionsPayload({
|
|
1216
|
+
restaurantId: input.restaurantId,
|
|
1217
|
+
menuId: input.menuId,
|
|
1218
|
+
currency: input.currency,
|
|
1219
|
+
consumerId: input.consumerId,
|
|
1220
|
+
optionLists: childOptionLists,
|
|
1221
|
+
selections: selection.children ?? [],
|
|
1222
|
+
mode: "standalone-child",
|
|
1223
|
+
resolveNestedOptionLists: input.resolveNestedOptionLists,
|
|
1224
|
+
});
|
|
1225
|
+
lowPriorityItems.push({
|
|
1226
|
+
storeId: input.restaurantId,
|
|
1227
|
+
menuId: input.menuId,
|
|
1228
|
+
itemId: option.id,
|
|
1229
|
+
itemName: option.name,
|
|
1230
|
+
currency: input.currency,
|
|
1231
|
+
quantity,
|
|
1232
|
+
unitPrice: option.unitAmount ?? 0,
|
|
1233
|
+
nestedOptions: childPayload.nestedOptions,
|
|
1234
|
+
});
|
|
1235
|
+
lowPriorityItems.push(...childPayload.lowPriorityItems);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
return {
|
|
1239
|
+
nestedOptions,
|
|
1240
|
+
lowPriorityItems,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function buildRegularLeafOption(input) {
|
|
1244
|
+
return {
|
|
1245
|
+
id: input.option.id,
|
|
1246
|
+
quantity: input.quantity,
|
|
1247
|
+
options: [],
|
|
1248
|
+
itemExtraOption: {
|
|
1249
|
+
id: input.option.id,
|
|
1250
|
+
name: input.option.name,
|
|
1251
|
+
description: input.option.name,
|
|
1252
|
+
price: input.option.unitAmount ?? 0,
|
|
1253
|
+
itemExtraName: null,
|
|
1254
|
+
chargeAbove: 0,
|
|
1255
|
+
defaultQuantity: input.option.defaultQuantity ?? 0,
|
|
1256
|
+
itemExtraId: input.group.id,
|
|
1257
|
+
itemExtraNumFreeOptions: input.group.numFreeOptions,
|
|
1258
|
+
menuItemExtraOptionPrice: input.option.unitAmount ?? 0,
|
|
1259
|
+
menuItemExtraOptionBasePrice: null,
|
|
1260
|
+
},
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function buildStandaloneChildLeafOption(input) {
|
|
1264
|
+
return {
|
|
1265
|
+
id: input.option.id,
|
|
1266
|
+
quantity: input.quantity,
|
|
1267
|
+
options: [],
|
|
1268
|
+
itemExtraOption: {
|
|
1269
|
+
id: input.option.id,
|
|
1270
|
+
name: input.option.name,
|
|
1271
|
+
description: input.option.name,
|
|
1272
|
+
price: input.option.unitAmount ?? 0,
|
|
1273
|
+
chargeAbove: 0,
|
|
1274
|
+
defaultQuantity: input.option.defaultQuantity ?? 0,
|
|
1275
|
+
},
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function validateSelectedOptionCounts(optionLists, selectionsByGroup) {
|
|
1279
|
+
for (const group of optionLists) {
|
|
1280
|
+
const selectedEntries = selectionsByGroup.get(group.id) ?? [];
|
|
1281
|
+
const selectedCount = selectedEntries.reduce((sum, entry) => sum + entry.quantity, 0);
|
|
1282
|
+
if (selectedCount < group.minNumOptions) {
|
|
1283
|
+
throw new Error(`Missing required selections for ${group.name}. Need at least ${group.minNumOptions}.`);
|
|
1284
|
+
}
|
|
1285
|
+
if (group.maxNumOptions > 0 && selectedCount > group.maxNumOptions) {
|
|
1286
|
+
throw new Error(`Too many selections for ${group.name}. Maximum is ${group.maxNumOptions}.`);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function isStandaloneRecommendedGroup(group) {
|
|
1291
|
+
return group.id.startsWith("recommended_option_");
|
|
1292
|
+
}
|
|
1293
|
+
function parseRequestedOptionSelection(entry, label) {
|
|
1294
|
+
const object = asObject(entry);
|
|
1295
|
+
const groupId = typeof object.groupId === "string" ? object.groupId.trim() : "";
|
|
1296
|
+
const optionId = typeof object.optionId === "string" ? object.optionId.trim() : "";
|
|
1297
|
+
const quantity = object.quantity == null ? undefined : Number.parseInt(String(object.quantity), 10);
|
|
1298
|
+
const childrenRaw = object.children;
|
|
1299
|
+
if (!groupId || !optionId) {
|
|
1300
|
+
throw new Error(`Invalid option selection at ${label}. Each entry must include string groupId and optionId fields.`);
|
|
1301
|
+
}
|
|
1302
|
+
if (quantity !== undefined && (!Number.isInteger(quantity) || quantity < 1)) {
|
|
1303
|
+
throw new Error(`Invalid option quantity at ${label}: ${object.quantity}`);
|
|
1304
|
+
}
|
|
1305
|
+
if (childrenRaw !== undefined && !Array.isArray(childrenRaw)) {
|
|
1306
|
+
throw new Error(`Invalid option children at ${label}. children must be an array when provided.`);
|
|
1307
|
+
}
|
|
1308
|
+
const children = Array.isArray(childrenRaw)
|
|
1309
|
+
? childrenRaw.map((child, index) => parseRequestedOptionSelection(child, `${label}.children[${index}]`))
|
|
1310
|
+
: undefined;
|
|
1311
|
+
return {
|
|
1312
|
+
groupId,
|
|
1313
|
+
optionId,
|
|
1314
|
+
...(quantity === undefined ? {} : { quantity }),
|
|
1315
|
+
...(children === undefined ? {} : { children }),
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeRequestedOptionSelections(selections) {
|
|
1319
|
+
const aggregated = new Map();
|
|
1320
|
+
for (const selection of selections) {
|
|
1321
|
+
const groupId = selection.groupId.trim();
|
|
1322
|
+
const optionId = selection.optionId.trim();
|
|
1323
|
+
const quantity = selection.quantity ?? 1;
|
|
1324
|
+
const children = selection.children ? normalizeRequestedOptionSelections(selection.children) : undefined;
|
|
1325
|
+
if (!groupId || !optionId) {
|
|
1326
|
+
throw new Error("Option selections must include non-empty groupId and optionId values.");
|
|
1327
|
+
}
|
|
1328
|
+
if (!Number.isInteger(quantity) || quantity < 1) {
|
|
1329
|
+
throw new Error(`Invalid option quantity for ${groupId}/${optionId}: ${selection.quantity}`);
|
|
1330
|
+
}
|
|
1331
|
+
const key = `${groupId}:${optionId}`;
|
|
1332
|
+
const previous = aggregated.get(key);
|
|
1333
|
+
if (previous) {
|
|
1334
|
+
if ((previous.children && previous.children.length > 0) || (children && children.length > 0)) {
|
|
1335
|
+
throw new Error(`Duplicate option selections for ${groupId}/${optionId} are only supported when no nested child selections are attached.`);
|
|
1336
|
+
}
|
|
1337
|
+
aggregated.set(key, { ...previous, quantity: (previous.quantity ?? 1) + quantity });
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
aggregated.set(key, { groupId, optionId, quantity, ...(children ? { children } : {}) });
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return [...aggregated.values()];
|
|
1344
|
+
}
|
|
1345
|
+
async function fetchNestedOptionListsDirect(input) {
|
|
1346
|
+
if (!input.option.nextCursor) {
|
|
1347
|
+
return [];
|
|
1348
|
+
}
|
|
1349
|
+
const data = await session.graphql("itemPage", ITEM_QUERY, {
|
|
1350
|
+
storeId: input.restaurantId,
|
|
1351
|
+
itemId: input.option.id,
|
|
1352
|
+
consumerId: input.consumerId,
|
|
1353
|
+
isMerchantPreview: false,
|
|
1354
|
+
isNested: true,
|
|
1355
|
+
fulfillmentType: "Delivery",
|
|
1356
|
+
cursorContext: {
|
|
1357
|
+
itemCursor: input.option.nextCursor,
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1360
|
+
const root = asObject(data.itemPage);
|
|
1361
|
+
const optionLists = Array.isArray(root.optionLists) ? root.optionLists : [];
|
|
1362
|
+
return optionLists.map(parseOptionList);
|
|
1363
|
+
}
|
|
1364
|
+
async function getAvailableAddressesDirect() {
|
|
1365
|
+
const data = await session.graphql("getAvailableAddresses", GET_AVAILABLE_ADDRESSES_QUERY, {});
|
|
1366
|
+
return Array.isArray(data.getAvailableAddresses) ? data.getAvailableAddresses : [];
|
|
1367
|
+
}
|
|
1368
|
+
async function autocompleteAddressDirect(inputAddress) {
|
|
1369
|
+
const params = new URLSearchParams({
|
|
1370
|
+
input_address: inputAddress,
|
|
1371
|
+
autocomplete_type: "AUTOCOMPLETE_TYPE_V2_UNSPECIFIED",
|
|
1372
|
+
});
|
|
1373
|
+
const response = await session.requestJson({
|
|
1374
|
+
url: `${BASE_URL}/unified-gateway/geo-intelligence/v2/address/autocomplete?${params.toString()}`,
|
|
1375
|
+
method: "GET",
|
|
1376
|
+
headers: { accept: "application/json" },
|
|
1377
|
+
operationName: "address autocomplete",
|
|
1378
|
+
});
|
|
1379
|
+
return Array.isArray(response.predictions) ? response.predictions : [];
|
|
1380
|
+
}
|
|
1381
|
+
async function getOrCreateAddressDirect(prediction) {
|
|
1382
|
+
const sourcePlaceId = prediction.source_place_id?.trim();
|
|
1383
|
+
if (!sourcePlaceId) {
|
|
1384
|
+
throw new Error("DoorDash autocomplete did not return a source_place_id for the selected address.");
|
|
1385
|
+
}
|
|
1386
|
+
const response = await session.requestJson({
|
|
1387
|
+
url: `${BASE_URL}/unified-gateway/geo-intelligence/v2/address/get-or-create`,
|
|
1388
|
+
method: "POST",
|
|
1389
|
+
headers: {
|
|
1390
|
+
accept: "application/json",
|
|
1391
|
+
"content-type": "application/json",
|
|
1392
|
+
},
|
|
1393
|
+
body: JSON.stringify({
|
|
1394
|
+
address_identifier: {
|
|
1395
|
+
_type: "source_place_id_request",
|
|
1396
|
+
source_place_id: sourcePlaceId,
|
|
1397
|
+
},
|
|
1398
|
+
}),
|
|
1399
|
+
operationName: "address get-or-create",
|
|
1400
|
+
});
|
|
1401
|
+
return response.address ?? null;
|
|
1402
|
+
}
|
|
1403
|
+
export function buildAddConsumerAddressPayload(input) {
|
|
1404
|
+
const createdAddress = input.createdAddress;
|
|
1405
|
+
const lat = typeof createdAddress?.lat === "number" ? createdAddress.lat : input.prediction.lat;
|
|
1406
|
+
const lng = typeof createdAddress?.lng === "number" ? createdAddress.lng : input.prediction.lng;
|
|
1407
|
+
const city = firstNonEmptyString(createdAddress?.locality, input.prediction.locality);
|
|
1408
|
+
const state = firstNonEmptyString(createdAddress?.administrative_area_level1, input.prediction.administrative_area_level1);
|
|
1409
|
+
const zipCode = firstNonEmptyString(createdAddress?.postal_code, combinePostalCode(input.prediction));
|
|
1410
|
+
const printableAddress = firstNonEmptyString(createdAddress?.formatted_address, input.prediction.formatted_address, input.requestedAddress);
|
|
1411
|
+
const shortname = firstNonEmptyString(createdAddress?.formatted_address_short, input.prediction.formatted_address_short, input.requestedAddress);
|
|
1412
|
+
const googlePlaceId = firstNonEmptyString(input.prediction.source_place_id);
|
|
1413
|
+
if (typeof lat !== "number" || typeof lng !== "number") {
|
|
1414
|
+
throw new Error(`DoorDash did not return stable coordinates for "${input.requestedAddress}".`);
|
|
1415
|
+
}
|
|
1416
|
+
if (!city || !state || !zipCode || !printableAddress || !shortname || !googlePlaceId) {
|
|
1417
|
+
throw new Error(`DoorDash resolved "${input.requestedAddress}", but the addConsumerAddressV2 payload was incomplete. city=${city ?? ""} state=${state ?? ""} zip=${zipCode ?? ""} shortname=${shortname ?? ""} googlePlaceId=${googlePlaceId ? "present" : "missing"}`);
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
lat,
|
|
1421
|
+
lng,
|
|
1422
|
+
city,
|
|
1423
|
+
state,
|
|
1424
|
+
zipCode,
|
|
1425
|
+
printableAddress,
|
|
1426
|
+
shortname,
|
|
1427
|
+
googlePlaceId,
|
|
1428
|
+
subpremise: null,
|
|
1429
|
+
driverInstructions: null,
|
|
1430
|
+
dropoffOptionId: null,
|
|
1431
|
+
manualLat: null,
|
|
1432
|
+
manualLng: null,
|
|
1433
|
+
addressLinkType: "ADDRESS_LINK_TYPE_UNSPECIFIED",
|
|
1434
|
+
buildingName: null,
|
|
1435
|
+
entryCode: null,
|
|
1436
|
+
personalAddressLabel: null,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
async function addConsumerAddressDirect(requestedAddress, payload) {
|
|
1440
|
+
const data = await session.graphql("addConsumerAddressV2", ADD_CONSUMER_ADDRESS_MUTATION, payload);
|
|
1441
|
+
const defaultAddress = data.addConsumerAddressV2?.defaultAddress ?? null;
|
|
1442
|
+
const matchedAddressId = typeof defaultAddress?.id === "string" ? defaultAddress.id : "";
|
|
1443
|
+
if (!matchedAddressId) {
|
|
1444
|
+
throw new Error(`DoorDash accepted addConsumerAddressV2 for "${requestedAddress}", but it did not return a saved defaultAddress id. The CLI is refusing to guess follow-up address state.`);
|
|
1445
|
+
}
|
|
1446
|
+
await session.saveState();
|
|
1447
|
+
return {
|
|
1448
|
+
success: true,
|
|
1449
|
+
mode: "direct-added-address",
|
|
1450
|
+
requestedAddress,
|
|
1451
|
+
matchedAddressId,
|
|
1452
|
+
matchedAddressSource: "add-consumer-address",
|
|
1453
|
+
printableAddress: defaultAddress?.printableAddress ?? payload.printableAddress,
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
async function updateConsumerDefaultAddressDirect(requestedAddress, match) {
|
|
1457
|
+
const data = await session.graphql("updateConsumerDefaultAddressV2", UPDATE_CONSUMER_DEFAULT_ADDRESS_MUTATION, { defaultAddressId: match.id });
|
|
1458
|
+
await session.saveState();
|
|
1459
|
+
return {
|
|
1460
|
+
success: true,
|
|
1461
|
+
mode: "direct-saved-address",
|
|
1462
|
+
requestedAddress,
|
|
1463
|
+
matchedAddressId: match.id,
|
|
1464
|
+
matchedAddressSource: match.source,
|
|
1465
|
+
printableAddress: data.updateConsumerDefaultAddressV2?.defaultAddress?.printableAddress ?? match.printableAddress ?? null,
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
export function resolveAvailableAddressMatch(input) {
|
|
1469
|
+
const addressIds = [input.prediction?.geo_address_id, input.createdAddress?.id]
|
|
1470
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1471
|
+
.map((value) => value.trim());
|
|
1472
|
+
for (const availableAddress of input.availableAddresses) {
|
|
1473
|
+
const defaultAddressId = typeof availableAddress.id === "string" ? availableAddress.id.trim() : "";
|
|
1474
|
+
if (!defaultAddressId) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (typeof availableAddress.addressId === "string" && addressIds.includes(availableAddress.addressId.trim())) {
|
|
1478
|
+
return {
|
|
1479
|
+
id: defaultAddressId,
|
|
1480
|
+
printableAddress: availableAddress.printableAddress ?? null,
|
|
1481
|
+
source: "autocomplete-address-id",
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const normalizedCandidates = dedupeBy([
|
|
1486
|
+
input.input,
|
|
1487
|
+
input.prediction?.formatted_address ?? null,
|
|
1488
|
+
input.prediction?.formatted_address_short ?? null,
|
|
1489
|
+
input.createdAddress?.formatted_address ?? null,
|
|
1490
|
+
input.createdAddress?.formatted_address_short ?? null,
|
|
1491
|
+
]
|
|
1492
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1493
|
+
.map(normalizeAddressText), (value) => value);
|
|
1494
|
+
for (const availableAddress of input.availableAddresses) {
|
|
1495
|
+
const defaultAddressId = typeof availableAddress.id === "string" ? availableAddress.id.trim() : "";
|
|
1496
|
+
if (!defaultAddressId) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
const printableAddress = normalizeAddressText(availableAddress.printableAddress ?? "");
|
|
1500
|
+
const shortname = normalizeAddressText(availableAddress.shortname ?? "");
|
|
1501
|
+
const matchesText = normalizedCandidates.some((candidate) => candidate === printableAddress ||
|
|
1502
|
+
candidate === shortname ||
|
|
1503
|
+
(shortname.length > 0 && candidate.includes(shortname)) ||
|
|
1504
|
+
(printableAddress.length > 0 && printableAddress.includes(candidate)));
|
|
1505
|
+
if (matchesText) {
|
|
1506
|
+
return {
|
|
1507
|
+
id: defaultAddressId,
|
|
1508
|
+
printableAddress: availableAddress.printableAddress ?? null,
|
|
1509
|
+
source: input.prediction || input.createdAddress ? "autocomplete-text" : "saved-address",
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return null;
|
|
1514
|
+
}
|
|
1515
|
+
function combinePostalCode(prediction) {
|
|
1516
|
+
const postalCode = typeof prediction.postal_code === "string" ? prediction.postal_code.trim() : "";
|
|
1517
|
+
const suffix = typeof prediction.postal_code_suffix === "string" ? prediction.postal_code_suffix.trim() : "";
|
|
1518
|
+
if (!postalCode) {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
return suffix ? `${postalCode}-${suffix}` : postalCode;
|
|
1522
|
+
}
|
|
1523
|
+
function firstNonEmptyString(...values) {
|
|
1524
|
+
for (const value of values) {
|
|
1525
|
+
if (typeof value === "string" && value.trim()) {
|
|
1526
|
+
return value.trim();
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
function normalizeAddressText(value) {
|
|
1532
|
+
return value
|
|
1533
|
+
.trim()
|
|
1534
|
+
.toLowerCase()
|
|
1535
|
+
.replace(/[.,]/g, "")
|
|
1536
|
+
.replace(/\s+/g, " ");
|
|
1537
|
+
}
|
|
1538
|
+
async function importManagedBrowserSessionIfAvailable() {
|
|
1539
|
+
for (const cdpUrl of await getManagedBrowserCdpCandidates()) {
|
|
1540
|
+
if (!(await isCdpEndpointReachable(cdpUrl))) {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
let browser = null;
|
|
1544
|
+
let tempPage = null;
|
|
1545
|
+
try {
|
|
1546
|
+
browser = await chromium.connectOverCDP(cdpUrl);
|
|
1547
|
+
const context = browser.contexts()[0];
|
|
1548
|
+
if (!context) {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
let page = context.pages().find((candidate) => candidate.url().includes("doordash.com")) ?? null;
|
|
1552
|
+
if (!page) {
|
|
1553
|
+
tempPage = await context.newPage();
|
|
1554
|
+
page = tempPage;
|
|
1555
|
+
await page.goto(`${BASE_URL}/home`, { waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => { });
|
|
1556
|
+
await page.waitForTimeout(1_000);
|
|
1557
|
+
}
|
|
1558
|
+
const consumerData = await fetchConsumerViaPage(page);
|
|
1559
|
+
const consumer = consumerData.consumer ?? null;
|
|
1560
|
+
if (!consumer || consumer.isGuest !== false) {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
await saveContextState(context);
|
|
1564
|
+
return true;
|
|
1565
|
+
}
|
|
1566
|
+
catch {
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
finally {
|
|
1570
|
+
await tempPage?.close().catch(() => { });
|
|
1571
|
+
await browser?.close().catch(() => { });
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
async function fetchConsumerViaPage(page) {
|
|
1577
|
+
const raw = await page.evaluate(async ({ query, headers, url }) => {
|
|
1578
|
+
const response = await fetch(url, {
|
|
1579
|
+
method: "POST",
|
|
1580
|
+
headers,
|
|
1581
|
+
body: JSON.stringify({ operationName: "consumer", variables: {}, query }),
|
|
1582
|
+
});
|
|
1583
|
+
return { status: response.status, text: await response.text() };
|
|
1584
|
+
}, {
|
|
1585
|
+
query: CONSUMER_QUERY,
|
|
1586
|
+
headers: GRAPHQL_HEADERS,
|
|
1587
|
+
url: `${BASE_URL}/graphql/consumer?operation=consumer`,
|
|
1588
|
+
});
|
|
1589
|
+
return parseGraphQlResponse("managedBrowserConsumerImport", raw.status, raw.text);
|
|
1590
|
+
}
|
|
1591
|
+
async function saveContextState(context) {
|
|
1592
|
+
const storageStatePath = getStorageStatePath();
|
|
1593
|
+
await ensureConfigDir();
|
|
1594
|
+
await context.storageState({ path: storageStatePath });
|
|
1595
|
+
const cookies = await context.cookies();
|
|
1596
|
+
await writeFile(getCookiesPath(), JSON.stringify(cookies, null, 2));
|
|
1597
|
+
}
|
|
1598
|
+
async function getManagedBrowserCdpCandidates() {
|
|
1599
|
+
const candidates = new Set();
|
|
1600
|
+
for (const value of [
|
|
1601
|
+
process.env.DOORDASH_MANAGED_BROWSER_CDP_URL,
|
|
1602
|
+
process.env.OPENCLAW_BROWSER_CDP_URL,
|
|
1603
|
+
process.env.OPENCLAW_OPENCLAW_CDP_URL,
|
|
1604
|
+
]) {
|
|
1605
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1606
|
+
candidates.add(value.trim().replace(/\/$/, ""));
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
for (const value of await readOpenClawBrowserConfigCandidates()) {
|
|
1610
|
+
candidates.add(value.replace(/\/$/, ""));
|
|
1611
|
+
}
|
|
1612
|
+
candidates.add("http://127.0.0.1:18800");
|
|
1613
|
+
return [...candidates];
|
|
1614
|
+
}
|
|
1615
|
+
async function readOpenClawBrowserConfigCandidates() {
|
|
1616
|
+
try {
|
|
1617
|
+
const raw = await readFile(join(homedir(), ".openclaw", "openclaw.json"), "utf8");
|
|
1618
|
+
const parsed = safeJsonParse(raw);
|
|
1619
|
+
const browserConfig = asObject(parsed?.browser);
|
|
1620
|
+
const candidates = [];
|
|
1621
|
+
const pushCandidate = (value) => {
|
|
1622
|
+
const object = asObject(value);
|
|
1623
|
+
if (typeof object.cdpUrl === "string" && object.cdpUrl.trim()) {
|
|
1624
|
+
candidates.push(object.cdpUrl.trim());
|
|
1625
|
+
}
|
|
1626
|
+
else if (typeof object.cdpPort === "number" && Number.isInteger(object.cdpPort)) {
|
|
1627
|
+
candidates.push(`http://127.0.0.1:${object.cdpPort}`);
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
pushCandidate(browserConfig);
|
|
1631
|
+
pushCandidate(browserConfig.openclaw);
|
|
1632
|
+
pushCandidate(asObject(browserConfig.profiles).openclaw);
|
|
1633
|
+
return dedupeBy(candidates, (value) => value);
|
|
1634
|
+
}
|
|
1635
|
+
catch {
|
|
1636
|
+
return [];
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
async function isCdpEndpointReachable(cdpUrl) {
|
|
1640
|
+
try {
|
|
1641
|
+
const response = await fetch(`${cdpUrl.replace(/\/$/, "")}/json/version`);
|
|
1642
|
+
return response.ok;
|
|
1643
|
+
}
|
|
1644
|
+
catch {
|
|
1645
|
+
return false;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
export function parseSearchRestaurants(body) {
|
|
1649
|
+
const results = [];
|
|
1650
|
+
for (const section of body) {
|
|
1651
|
+
const entries = asObject(section).body;
|
|
1652
|
+
if (!Array.isArray(entries)) {
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
for (const entry of entries) {
|
|
1656
|
+
const parsed = parseSearchRestaurantRow(entry);
|
|
1657
|
+
if (parsed) {
|
|
1658
|
+
results.push(parsed);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return dedupeBy(results, (row) => row.id);
|
|
1663
|
+
}
|
|
1664
|
+
export function parseSearchRestaurantRow(entry) {
|
|
1665
|
+
const object = asObject(entry);
|
|
1666
|
+
const componentId = asObject(object.component).id;
|
|
1667
|
+
if (componentId !== "row.store") {
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
const title = asObject(object.text).title;
|
|
1671
|
+
if (typeof title !== "string" || title.trim().length === 0) {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
const customPairs = Array.isArray(asObject(object.text).custom) ? asObject(object.text).custom : [];
|
|
1675
|
+
const custom = new Map();
|
|
1676
|
+
for (const pair of customPairs) {
|
|
1677
|
+
const pairObject = asObject(pair);
|
|
1678
|
+
if (typeof pairObject.key === "string" && typeof pairObject.value === "string") {
|
|
1679
|
+
custom.set(pairObject.key, pairObject.value);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const clickData = safeJsonParse(asObject(asObject(object.events).click).data);
|
|
1683
|
+
const url = clickData?.uri ? `${BASE_URL}/${clickData.uri.replace(/^\//, "")}` : null;
|
|
1684
|
+
const id = extractStoreId(url) ?? extractIdFromFacetId(typeof object.id === "string" ? object.id : "") ?? title.trim();
|
|
1685
|
+
const description = typeof asObject(object.text).description === "string" ? asObject(object.text).description : null;
|
|
1686
|
+
return {
|
|
1687
|
+
id,
|
|
1688
|
+
name: title.trim(),
|
|
1689
|
+
description,
|
|
1690
|
+
cuisines: parseCuisineDescription(description),
|
|
1691
|
+
isRetail: custom.get("is_retail") === "true",
|
|
1692
|
+
eta: custom.get("eta_display_string") ?? custom.get("cc_eta_string") ?? null,
|
|
1693
|
+
deliveryFee: custom.get("delivery_fee_string") ?? custom.get("modality_display_string") ?? null,
|
|
1694
|
+
imageUrl: asObject(asObject(object.images).main).uri ?? null,
|
|
1695
|
+
url,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
function parseMenuResponse(storepageFeed, requestedRestaurantId) {
|
|
1699
|
+
const root = asObject(storepageFeed);
|
|
1700
|
+
const storeHeader = asObject(root.storeHeader);
|
|
1701
|
+
const menuBook = asObject(root.menuBook);
|
|
1702
|
+
const itemLists = Array.isArray(root.itemLists) ? root.itemLists : [];
|
|
1703
|
+
const categories = itemLists.map((list) => {
|
|
1704
|
+
const object = asObject(list);
|
|
1705
|
+
const items = Array.isArray(object.items) ? object.items : [];
|
|
1706
|
+
return {
|
|
1707
|
+
id: typeof object.id === "string" ? object.id : "",
|
|
1708
|
+
name: typeof object.name === "string" ? object.name : "",
|
|
1709
|
+
description: typeof object.description === "string" ? object.description : null,
|
|
1710
|
+
itemCount: items.length,
|
|
1711
|
+
items: items.map(parseMenuItem).filter((item) => item !== null),
|
|
1712
|
+
};
|
|
1713
|
+
});
|
|
1714
|
+
return {
|
|
1715
|
+
success: true,
|
|
1716
|
+
restaurant: {
|
|
1717
|
+
id: typeof storeHeader.id === "string" ? storeHeader.id : requestedRestaurantId,
|
|
1718
|
+
name: typeof storeHeader.name === "string" ? storeHeader.name : null,
|
|
1719
|
+
description: typeof storeHeader.description === "string" ? storeHeader.description : null,
|
|
1720
|
+
businessName: asObject(storeHeader.business).name ?? null,
|
|
1721
|
+
displayAddress: asObject(storeHeader.address).displayAddress ?? null,
|
|
1722
|
+
averageRating: typeof asObject(storeHeader.ratings).averageRating === "number"
|
|
1723
|
+
? asObject(storeHeader.ratings).averageRating
|
|
1724
|
+
: null,
|
|
1725
|
+
numRatingsDisplayString: asObject(storeHeader.ratings).numRatingsDisplayString ?? null,
|
|
1726
|
+
coverImgUrl: typeof storeHeader.coverImgUrl === "string" ? storeHeader.coverImgUrl : null,
|
|
1727
|
+
},
|
|
1728
|
+
menu: {
|
|
1729
|
+
id: typeof menuBook.id === "string" ? menuBook.id : null,
|
|
1730
|
+
name: typeof menuBook.name === "string" ? menuBook.name : null,
|
|
1731
|
+
},
|
|
1732
|
+
categoryCount: categories.length,
|
|
1733
|
+
itemCount: categories.reduce((sum, category) => sum + category.items.length, 0),
|
|
1734
|
+
categories,
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function parseItemResponse(itemPage, restaurantId) {
|
|
1738
|
+
const root = asObject(itemPage);
|
|
1739
|
+
const itemHeader = asObject(root.itemHeader);
|
|
1740
|
+
const optionLists = Array.isArray(root.optionLists) ? root.optionLists : [];
|
|
1741
|
+
const parsedOptionLists = optionLists.map(parseOptionList);
|
|
1742
|
+
return {
|
|
1743
|
+
success: true,
|
|
1744
|
+
restaurantId,
|
|
1745
|
+
item: {
|
|
1746
|
+
id: typeof itemHeader.id === "string" ? itemHeader.id : null,
|
|
1747
|
+
name: typeof itemHeader.name === "string" ? itemHeader.name : null,
|
|
1748
|
+
description: typeof itemHeader.description === "string" ? itemHeader.description : null,
|
|
1749
|
+
displayPrice: typeof itemHeader.displayString === "string" ? itemHeader.displayString : null,
|
|
1750
|
+
unitAmount: typeof itemHeader.unitAmount === "number" ? itemHeader.unitAmount : null,
|
|
1751
|
+
currency: typeof itemHeader.currency === "string" ? itemHeader.currency : null,
|
|
1752
|
+
decimalPlaces: typeof itemHeader.decimalPlaces === "number" ? itemHeader.decimalPlaces : null,
|
|
1753
|
+
menuId: typeof itemHeader.menuId === "string" ? itemHeader.menuId : null,
|
|
1754
|
+
specialInstructionsMaxLength: typeof itemHeader.specialInstructionsMaxLength === "number" ? itemHeader.specialInstructionsMaxLength : null,
|
|
1755
|
+
dietaryTags: Array.isArray(itemHeader.dietaryTagsList)
|
|
1756
|
+
? itemHeader.dietaryTagsList.map((tag) => {
|
|
1757
|
+
const object = asObject(tag);
|
|
1758
|
+
return {
|
|
1759
|
+
type: typeof object.type === "string" ? object.type : null,
|
|
1760
|
+
abbreviatedTagDisplayString: typeof object.abbreviatedTagDisplayString === "string" ? object.abbreviatedTagDisplayString : null,
|
|
1761
|
+
fullTagDisplayString: typeof object.fullTagDisplayString === "string" ? object.fullTagDisplayString : null,
|
|
1762
|
+
};
|
|
1763
|
+
})
|
|
1764
|
+
: [],
|
|
1765
|
+
reviewData: root.itemHeader
|
|
1766
|
+
? {
|
|
1767
|
+
ratingDisplayString: asObject(itemHeader.reviewData).ratingDisplayString ?? null,
|
|
1768
|
+
reviewCount: asObject(itemHeader.reviewData).reviewCount ?? null,
|
|
1769
|
+
itemReviewRankingCount: typeof asObject(itemHeader.reviewData).itemReviewRankingCount === "number"
|
|
1770
|
+
? asObject(itemHeader.reviewData).itemReviewRankingCount
|
|
1771
|
+
: null,
|
|
1772
|
+
}
|
|
1773
|
+
: null,
|
|
1774
|
+
requiredOptionLists: parsedOptionLists.filter((group) => group.minNumOptions > 0 && !group.isOptional),
|
|
1775
|
+
optionLists: parsedOptionLists,
|
|
1776
|
+
preferences: Array.isArray(root.itemPreferences) ? root.itemPreferences : [],
|
|
1777
|
+
},
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function parseCartResponse(cartRoot) {
|
|
1781
|
+
const cart = cartRoot ? asObject(cartRoot) : {};
|
|
1782
|
+
const orders = Array.isArray(cart.orders) ? cart.orders : [];
|
|
1783
|
+
const items = orders.flatMap((order) => {
|
|
1784
|
+
const orderItems = Array.isArray(asObject(order).orderItems) ? asObject(order).orderItems : [];
|
|
1785
|
+
return orderItems.map((item) => {
|
|
1786
|
+
const object = asObject(item);
|
|
1787
|
+
return {
|
|
1788
|
+
cartItemId: typeof object.id === "string" ? object.id : "",
|
|
1789
|
+
itemId: asObject(object.item).id ?? null,
|
|
1790
|
+
name: asObject(object.item).name ?? null,
|
|
1791
|
+
quantity: typeof object.quantity === "number" ? object.quantity : 0,
|
|
1792
|
+
specialInstructions: typeof object.specialInstructions === "string" ? object.specialInstructions : object.specialInstructions ?? null,
|
|
1793
|
+
priceDisplayString: typeof object.priceDisplayString === "string" ? object.priceDisplayString : null,
|
|
1794
|
+
singlePrice: typeof object.singlePrice === "number" ? object.singlePrice : null,
|
|
1795
|
+
totalPrice: typeof object.priceOfTotalQuantity === "number" ? object.priceOfTotalQuantity : null,
|
|
1796
|
+
status: typeof object.cartItemStatusType === "string" ? object.cartItemStatusType : null,
|
|
1797
|
+
options: Array.isArray(object.options)
|
|
1798
|
+
? object.options.map((option) => {
|
|
1799
|
+
const optionObject = asObject(option);
|
|
1800
|
+
return {
|
|
1801
|
+
id: typeof optionObject.id === "string" ? optionObject.id : "",
|
|
1802
|
+
name: typeof optionObject.name === "string" ? optionObject.name : null,
|
|
1803
|
+
quantity: typeof optionObject.quantity === "number" ? optionObject.quantity : null,
|
|
1804
|
+
};
|
|
1805
|
+
})
|
|
1806
|
+
: [],
|
|
1807
|
+
};
|
|
1808
|
+
});
|
|
1809
|
+
});
|
|
1810
|
+
return {
|
|
1811
|
+
success: true,
|
|
1812
|
+
cartId: typeof cart.id === "string" ? cart.id : null,
|
|
1813
|
+
subtotal: typeof cart.subtotal === "number" ? cart.subtotal : null,
|
|
1814
|
+
total: typeof cart.total === "number" ? cart.total : null,
|
|
1815
|
+
currencyCode: typeof cart.currencyCode === "string" ? cart.currencyCode : null,
|
|
1816
|
+
restaurant: cart.restaurant
|
|
1817
|
+
? {
|
|
1818
|
+
id: asObject(cart.restaurant).id ?? null,
|
|
1819
|
+
name: asObject(cart.restaurant).name ?? null,
|
|
1820
|
+
slug: asObject(cart.restaurant).slug ?? null,
|
|
1821
|
+
businessName: asObject(asObject(cart.restaurant).business).name ?? null,
|
|
1822
|
+
}
|
|
1823
|
+
: null,
|
|
1824
|
+
menu: cart.menu
|
|
1825
|
+
? {
|
|
1826
|
+
id: asObject(cart.menu).id ?? null,
|
|
1827
|
+
name: asObject(cart.menu).name ?? null,
|
|
1828
|
+
}
|
|
1829
|
+
: null,
|
|
1830
|
+
itemCount: items.length,
|
|
1831
|
+
items,
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
export function parseExistingOrderLifecycleStatus(orderRoot) {
|
|
1835
|
+
const order = asObject(orderRoot);
|
|
1836
|
+
if (typeof order.cancelledAt === "string" && order.cancelledAt.length > 0) {
|
|
1837
|
+
return "cancelled";
|
|
1838
|
+
}
|
|
1839
|
+
if (typeof order.fulfilledAt === "string" && order.fulfilledAt.length > 0) {
|
|
1840
|
+
return "fulfilled";
|
|
1841
|
+
}
|
|
1842
|
+
if (typeof order.pollingInterval === "number" && order.pollingInterval > 0) {
|
|
1843
|
+
return "in-progress";
|
|
1844
|
+
}
|
|
1845
|
+
if (typeof order.submittedAt === "string" && order.submittedAt.length > 0) {
|
|
1846
|
+
return "submitted";
|
|
1847
|
+
}
|
|
1848
|
+
if (typeof order.createdAt === "string" && order.createdAt.length > 0) {
|
|
1849
|
+
return "draft";
|
|
1850
|
+
}
|
|
1851
|
+
return "unknown";
|
|
1852
|
+
}
|
|
1853
|
+
export function parseExistingOrdersResponse(orderRoots) {
|
|
1854
|
+
return orderRoots
|
|
1855
|
+
.map((order) => parseExistingOrder(order))
|
|
1856
|
+
.sort((left, right) => compareIsoDateDesc(left.createdAt, right.createdAt));
|
|
1857
|
+
}
|
|
1858
|
+
export function extractExistingOrdersFromApolloCache(cache) {
|
|
1859
|
+
if (!cache) {
|
|
1860
|
+
return [];
|
|
1861
|
+
}
|
|
1862
|
+
const rootQuery = asObject(cache.ROOT_QUERY);
|
|
1863
|
+
const key = Object.keys(rootQuery)
|
|
1864
|
+
.filter((entry) => entry.startsWith("getConsumerOrdersWithDetails("))
|
|
1865
|
+
.sort()[0];
|
|
1866
|
+
if (!key) {
|
|
1867
|
+
return [];
|
|
1868
|
+
}
|
|
1869
|
+
const values = Array.isArray(rootQuery[key]) ? rootQuery[key] : [];
|
|
1870
|
+
return values.map((value) => parseExistingOrder(value, cache)).sort((left, right) => compareIsoDateDesc(left.createdAt, right.createdAt));
|
|
1871
|
+
}
|
|
1872
|
+
function parseExistingOrder(orderRoot, cache = null) {
|
|
1873
|
+
const order = asObject(resolveApolloCacheValue(cache, orderRoot));
|
|
1874
|
+
const creator = order.creator ? asObject(resolveApolloCacheValue(cache, order.creator)) : null;
|
|
1875
|
+
const deliveryAddress = order.deliveryAddress ? asObject(resolveApolloCacheValue(cache, order.deliveryAddress)) : null;
|
|
1876
|
+
const store = order.store ? asObject(resolveApolloCacheValue(cache, order.store)) : null;
|
|
1877
|
+
const lifecycleStatus = parseExistingOrderLifecycleStatus(order);
|
|
1878
|
+
const items = parseExistingOrderItems(order.orders, cache);
|
|
1879
|
+
return {
|
|
1880
|
+
id: typeof order.id === "string" ? order.id : null,
|
|
1881
|
+
orderUuid: typeof order.orderUuid === "string" ? order.orderUuid : null,
|
|
1882
|
+
deliveryUuid: typeof order.deliveryUuid === "string" ? order.deliveryUuid : null,
|
|
1883
|
+
createdAt: typeof order.createdAt === "string" ? order.createdAt : null,
|
|
1884
|
+
submittedAt: typeof order.submittedAt === "string" ? order.submittedAt : null,
|
|
1885
|
+
cancelledAt: typeof order.cancelledAt === "string" ? order.cancelledAt : null,
|
|
1886
|
+
fulfilledAt: typeof order.fulfilledAt === "string" ? order.fulfilledAt : null,
|
|
1887
|
+
lifecycleStatus,
|
|
1888
|
+
isActive: lifecycleStatus === "submitted" || lifecycleStatus === "in-progress",
|
|
1889
|
+
hasLiveTracking: typeof order.pollingInterval === "number" && order.pollingInterval > 0,
|
|
1890
|
+
pollingIntervalSeconds: typeof order.pollingInterval === "number" ? order.pollingInterval : null,
|
|
1891
|
+
specialInstructions: typeof order.specialInstructions === "string" ? order.specialInstructions : null,
|
|
1892
|
+
isReorderable: Boolean(order.isReorderable),
|
|
1893
|
+
isGift: Boolean(order.isGift),
|
|
1894
|
+
isPickup: Boolean(order.isPickup),
|
|
1895
|
+
isRetail: Boolean(order.isRetail),
|
|
1896
|
+
isMerchantShipping: Boolean(order.isMerchantShipping),
|
|
1897
|
+
containsAlcohol: Boolean(order.containsAlcohol),
|
|
1898
|
+
fulfillmentType: typeof order.fulfillmentType === "string" ? order.fulfillmentType : null,
|
|
1899
|
+
shoppingProtocol: typeof order.shoppingProtocol === "string" ? order.shoppingProtocol : null,
|
|
1900
|
+
orderFilterType: typeof order.orderFilterType === "string" ? order.orderFilterType : null,
|
|
1901
|
+
creator: creator
|
|
1902
|
+
? {
|
|
1903
|
+
id: typeof creator.id === "string" ? creator.id : null,
|
|
1904
|
+
firstName: typeof creator.firstName === "string" ? creator.firstName : null,
|
|
1905
|
+
lastName: typeof creator.lastName === "string" ? creator.lastName : null,
|
|
1906
|
+
}
|
|
1907
|
+
: null,
|
|
1908
|
+
deliveryAddress: deliveryAddress
|
|
1909
|
+
? {
|
|
1910
|
+
id: typeof deliveryAddress.id === "string" ? deliveryAddress.id : null,
|
|
1911
|
+
formattedAddress: typeof deliveryAddress.formattedAddress === "string" ? deliveryAddress.formattedAddress : null,
|
|
1912
|
+
}
|
|
1913
|
+
: null,
|
|
1914
|
+
store: store
|
|
1915
|
+
? {
|
|
1916
|
+
id: typeof store.id === "string" ? store.id : null,
|
|
1917
|
+
name: typeof store.name === "string" ? store.name : null,
|
|
1918
|
+
businessName: typeof asObject(resolveApolloCacheValue(cache, store.business)).name === "string"
|
|
1919
|
+
? asObject(resolveApolloCacheValue(cache, store.business)).name
|
|
1920
|
+
: null,
|
|
1921
|
+
phoneNumber: typeof store.phoneNumber === "string" ? store.phoneNumber : null,
|
|
1922
|
+
fulfillsOwnDeliveries: typeof store.fulfillsOwnDeliveries === "boolean" ? store.fulfillsOwnDeliveries : null,
|
|
1923
|
+
customerArrivedPickupInstructions: typeof store.customerArrivedPickupInstructions === "string" ? store.customerArrivedPickupInstructions : null,
|
|
1924
|
+
rerouteStoreId: typeof store.rerouteStoreId === "string" ? store.rerouteStoreId : null,
|
|
1925
|
+
}
|
|
1926
|
+
: null,
|
|
1927
|
+
grandTotal: parseExistingOrderMoney(order.grandTotal, cache),
|
|
1928
|
+
itemCount: items.length,
|
|
1929
|
+
items,
|
|
1930
|
+
likelyOutOfStockItems: Array.isArray(order.likelyOosItems)
|
|
1931
|
+
? order.likelyOosItems.map((item) => {
|
|
1932
|
+
const object = asObject(resolveApolloCacheValue(cache, item));
|
|
1933
|
+
return {
|
|
1934
|
+
menuItemId: typeof object.menuItemId === "string" ? object.menuItemId : null,
|
|
1935
|
+
name: typeof object.name === "string" ? object.name : null,
|
|
1936
|
+
photoUrl: typeof object.photoUrl === "string" ? object.photoUrl : null,
|
|
1937
|
+
};
|
|
1938
|
+
})
|
|
1939
|
+
: [],
|
|
1940
|
+
recurringOrderDetails: order.recurringOrderDetails
|
|
1941
|
+
? {
|
|
1942
|
+
itemNames: Array.isArray(asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).itemNames)
|
|
1943
|
+
? asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).itemNames.filter((value) => typeof value === "string")
|
|
1944
|
+
: [],
|
|
1945
|
+
consumerId: typeof asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).consumerId === "string"
|
|
1946
|
+
? asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).consumerId
|
|
1947
|
+
: null,
|
|
1948
|
+
recurringOrderUpcomingOrderUuid: typeof asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).recurringOrderUpcomingOrderUuid === "string"
|
|
1949
|
+
? asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).recurringOrderUpcomingOrderUuid
|
|
1950
|
+
: null,
|
|
1951
|
+
scheduledDeliveryDate: typeof asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).scheduledDeliveryDate === "string"
|
|
1952
|
+
? asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).scheduledDeliveryDate
|
|
1953
|
+
: null,
|
|
1954
|
+
arrivalTimeDisplayString: typeof asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).arrivalTimeDisplayString === "string"
|
|
1955
|
+
? asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).arrivalTimeDisplayString
|
|
1956
|
+
: null,
|
|
1957
|
+
storeName: typeof asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).storeName === "string"
|
|
1958
|
+
? asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).storeName
|
|
1959
|
+
: null,
|
|
1960
|
+
isCancelled: typeof asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).isCancelled === "boolean"
|
|
1961
|
+
? asObject(resolveApolloCacheValue(cache, order.recurringOrderDetails)).isCancelled
|
|
1962
|
+
: null,
|
|
1963
|
+
}
|
|
1964
|
+
: null,
|
|
1965
|
+
bundleOrderInfo: order.bundleOrderInfo
|
|
1966
|
+
? {
|
|
1967
|
+
primaryBundleOrderUuid: typeof asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).primaryBundleOrderUuid === "string"
|
|
1968
|
+
? asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).primaryBundleOrderUuid
|
|
1969
|
+
: null,
|
|
1970
|
+
primaryBundleOrderId: typeof asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).primaryBundleOrderId === "string"
|
|
1971
|
+
? asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).primaryBundleOrderId
|
|
1972
|
+
: null,
|
|
1973
|
+
bundleOrderUuids: Array.isArray(asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).bundleOrderUuids)
|
|
1974
|
+
? asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).bundleOrderUuids.filter((value) => typeof value === "string")
|
|
1975
|
+
: [],
|
|
1976
|
+
bundleType: typeof asObject(resolveApolloCacheValue(cache, asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).bundleOrderConfig)).bundleType === "string"
|
|
1977
|
+
? asObject(resolveApolloCacheValue(cache, asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).bundleOrderConfig)).bundleType
|
|
1978
|
+
: null,
|
|
1979
|
+
bundleOrderRole: typeof asObject(resolveApolloCacheValue(cache, asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).bundleOrderConfig)).bundleOrderRole === "string"
|
|
1980
|
+
? asObject(resolveApolloCacheValue(cache, asObject(resolveApolloCacheValue(cache, order.bundleOrderInfo)).bundleOrderConfig)).bundleOrderRole
|
|
1981
|
+
: null,
|
|
1982
|
+
}
|
|
1983
|
+
: null,
|
|
1984
|
+
cancellationPendingRefundState: typeof asObject(resolveApolloCacheValue(cache, order.cancellationPendingRefundInfo)).state === "string"
|
|
1985
|
+
? asObject(resolveApolloCacheValue(cache, order.cancellationPendingRefundInfo)).state
|
|
1986
|
+
: null,
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function parseExistingOrderMoney(value, cache) {
|
|
1990
|
+
if (!value) {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
const object = asObject(resolveApolloCacheValue(cache, value));
|
|
1994
|
+
return {
|
|
1995
|
+
unitAmount: typeof object.unitAmount === "number" ? object.unitAmount : null,
|
|
1996
|
+
currency: typeof object.currency === "string" ? object.currency : null,
|
|
1997
|
+
decimalPlaces: typeof object.decimalPlaces === "number" ? object.decimalPlaces : null,
|
|
1998
|
+
displayString: typeof object.displayString === "string" ? object.displayString : null,
|
|
1999
|
+
sign: typeof object.sign === "string" ? object.sign : null,
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function parseExistingOrderItems(value, cache) {
|
|
2003
|
+
const groups = Array.isArray(value) ? value : [];
|
|
2004
|
+
return groups.flatMap((group) => {
|
|
2005
|
+
const object = asObject(resolveApolloCacheValue(cache, group));
|
|
2006
|
+
const items = Array.isArray(object.items) ? object.items : [];
|
|
2007
|
+
return items.map((item) => parseExistingOrderItem(item, cache));
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
function parseExistingOrderItem(value, cache) {
|
|
2011
|
+
const object = asObject(resolveApolloCacheValue(cache, value));
|
|
2012
|
+
return {
|
|
2013
|
+
id: typeof object.id === "string" ? object.id : null,
|
|
2014
|
+
name: typeof object.name === "string" ? object.name : null,
|
|
2015
|
+
quantity: typeof object.quantity === "number" ? object.quantity : 0,
|
|
2016
|
+
specialInstructions: typeof object.specialInstructions === "string" ? object.specialInstructions : null,
|
|
2017
|
+
substitutionPreferences: typeof object.substitutionPreferences === "string" ? object.substitutionPreferences : null,
|
|
2018
|
+
originalItemPrice: typeof object.originalItemPrice === "number" ? object.originalItemPrice : null,
|
|
2019
|
+
purchaseType: typeof object.purchaseType === "string" ? object.purchaseType : null,
|
|
2020
|
+
purchaseQuantity: parseExistingOrderQuantity(object.purchaseQuantity, cache),
|
|
2021
|
+
fulfillQuantity: parseExistingOrderQuantity(object.fulfillQuantity, cache),
|
|
2022
|
+
extras: Array.isArray(object.orderItemExtras)
|
|
2023
|
+
? object.orderItemExtras.map((extra) => parseExistingOrderExtra(extra, cache))
|
|
2024
|
+
: [],
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function parseExistingOrderQuantity(value, cache) {
|
|
2028
|
+
if (!value) {
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
const quantityRoot = asObject(resolveApolloCacheValue(cache, value));
|
|
2032
|
+
const continuous = asObject(resolveApolloCacheValue(cache, quantityRoot.continuousQuantity));
|
|
2033
|
+
const discrete = asObject(resolveApolloCacheValue(cache, quantityRoot.discreteQuantity));
|
|
2034
|
+
const candidate = typeof continuous.quantity === "number" ? continuous : discrete;
|
|
2035
|
+
return {
|
|
2036
|
+
quantity: typeof candidate.quantity === "number" ? candidate.quantity : null,
|
|
2037
|
+
unit: typeof candidate.unit === "string" ? candidate.unit : null,
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
function parseExistingOrderExtra(value, cache) {
|
|
2041
|
+
const object = asObject(resolveApolloCacheValue(cache, value));
|
|
2042
|
+
return {
|
|
2043
|
+
menuItemExtraId: typeof object.menuItemExtraId === "string" ? object.menuItemExtraId : null,
|
|
2044
|
+
name: typeof object.name === "string" ? object.name : null,
|
|
2045
|
+
options: Array.isArray(object.orderItemExtraOptions)
|
|
2046
|
+
? object.orderItemExtraOptions.map((option) => parseExistingOrderExtraOption(option, cache))
|
|
2047
|
+
: [],
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
function parseExistingOrderExtraOption(value, cache) {
|
|
2051
|
+
const object = asObject(resolveApolloCacheValue(cache, value));
|
|
2052
|
+
return {
|
|
2053
|
+
menuExtraOptionId: typeof object.menuExtraOptionId === "string" ? object.menuExtraOptionId : null,
|
|
2054
|
+
name: typeof object.name === "string" ? object.name : null,
|
|
2055
|
+
description: typeof object.description === "string" ? object.description : null,
|
|
2056
|
+
price: typeof object.price === "number" ? object.price : null,
|
|
2057
|
+
quantity: typeof object.quantity === "number" ? object.quantity : null,
|
|
2058
|
+
extras: Array.isArray(object.orderItemExtras)
|
|
2059
|
+
? object.orderItemExtras.map((extra) => parseExistingOrderExtra(extra, cache))
|
|
2060
|
+
: [],
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function parseOptionList(optionList) {
|
|
2064
|
+
const object = asObject(optionList);
|
|
2065
|
+
const options = Array.isArray(object.options) ? object.options : [];
|
|
2066
|
+
return {
|
|
2067
|
+
id: typeof object.id === "string" ? object.id : "",
|
|
2068
|
+
name: typeof object.name === "string" ? object.name : "",
|
|
2069
|
+
subtitle: typeof object.subtitle === "string" ? object.subtitle : null,
|
|
2070
|
+
minNumOptions: typeof object.minNumOptions === "number" ? object.minNumOptions : 0,
|
|
2071
|
+
maxNumOptions: typeof object.maxNumOptions === "number" ? object.maxNumOptions : 0,
|
|
2072
|
+
numFreeOptions: typeof object.numFreeOptions === "number" ? object.numFreeOptions : 0,
|
|
2073
|
+
isOptional: Boolean(object.isOptional),
|
|
2074
|
+
options: options.map((option) => {
|
|
2075
|
+
const optionObject = asObject(option);
|
|
2076
|
+
return {
|
|
2077
|
+
id: typeof optionObject.id === "string" ? optionObject.id : "",
|
|
2078
|
+
name: typeof optionObject.name === "string" ? optionObject.name : "",
|
|
2079
|
+
displayPrice: typeof optionObject.displayString === "string" ? optionObject.displayString : null,
|
|
2080
|
+
unitAmount: typeof optionObject.unitAmount === "number" ? optionObject.unitAmount : null,
|
|
2081
|
+
defaultQuantity: typeof optionObject.defaultQuantity === "number" ? optionObject.defaultQuantity : null,
|
|
2082
|
+
nextCursor: typeof optionObject.nextCursor === "string" ? optionObject.nextCursor : null,
|
|
2083
|
+
};
|
|
2084
|
+
}),
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
function parseMenuItem(item) {
|
|
2088
|
+
const object = asObject(item);
|
|
2089
|
+
if (typeof object.id !== "string" || typeof object.name !== "string") {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
return {
|
|
2093
|
+
id: object.id,
|
|
2094
|
+
name: object.name,
|
|
2095
|
+
description: typeof object.description === "string" ? object.description : null,
|
|
2096
|
+
displayPrice: typeof object.displayPrice === "string" ? object.displayPrice : null,
|
|
2097
|
+
imageUrl: typeof object.imageUrl === "string" ? object.imageUrl : null,
|
|
2098
|
+
nextCursor: typeof object.nextCursor === "string" ? object.nextCursor : null,
|
|
2099
|
+
storeId: typeof object.storeId === "string" ? object.storeId : null,
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
async function resolveMenuItem(restaurantId, itemId, itemName) {
|
|
2103
|
+
const menu = await getMenuDirect(restaurantId);
|
|
2104
|
+
const allItems = dedupeBy(menu.categories.flatMap((category) => category.items), (item) => item.id);
|
|
2105
|
+
const resolved = itemId
|
|
2106
|
+
? allItems.find((item) => item.id === itemId)
|
|
2107
|
+
: resolveMenuItemByName(allItems, itemName ?? "");
|
|
2108
|
+
if (!resolved) {
|
|
2109
|
+
throw new Error(itemId ? `Could not find item ${itemId} in restaurant ${restaurantId}.` : `Could not find item named \"${itemName}\".`);
|
|
2110
|
+
}
|
|
2111
|
+
const itemDetail = await getItemDirect(restaurantId, resolved.id);
|
|
2112
|
+
return { item: resolved, itemDetail };
|
|
2113
|
+
}
|
|
2114
|
+
function resolveMenuItemByName(items, itemName) {
|
|
2115
|
+
const normalized = normalizeItemName(itemName);
|
|
2116
|
+
const exactMatches = items.filter((item) => normalizeItemName(item.name) === normalized);
|
|
2117
|
+
if (exactMatches.length === 1) {
|
|
2118
|
+
return exactMatches[0];
|
|
2119
|
+
}
|
|
2120
|
+
if (exactMatches.length > 1) {
|
|
2121
|
+
throw new Error(`Multiple items matched \"${itemName}\". Use --item-id instead. Matching item IDs: ${exactMatches.map((item) => item.id).join(", ")}`);
|
|
2122
|
+
}
|
|
2123
|
+
const fuzzyMatches = items.filter((item) => normalizeItemName(item.name).includes(normalized));
|
|
2124
|
+
if (fuzzyMatches.length === 1) {
|
|
2125
|
+
return fuzzyMatches[0];
|
|
2126
|
+
}
|
|
2127
|
+
if (fuzzyMatches.length > 1) {
|
|
2128
|
+
throw new Error(`Multiple items partially matched \"${itemName}\". Use --item-id instead. Matching item IDs: ${fuzzyMatches.map((item) => item.id).join(", ")}`);
|
|
2129
|
+
}
|
|
2130
|
+
return undefined;
|
|
2131
|
+
}
|
|
2132
|
+
function parseGraphQlResponse(operationName, status, text) {
|
|
2133
|
+
const parsed = safeJsonParse(text);
|
|
2134
|
+
if (!parsed) {
|
|
2135
|
+
throw new Error(`DoorDash ${operationName} returned HTTP ${status} with a non-JSON response. This usually means the stored session or anti-bot state needs to be refreshed. Response snippet: ${truncate(text, 240)}`);
|
|
2136
|
+
}
|
|
2137
|
+
if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
|
|
2138
|
+
const message = parsed.errors.map((error) => error.message ?? "Unknown GraphQL error").join("; ");
|
|
2139
|
+
throw new Error(`DoorDash ${operationName} failed: ${message}`);
|
|
2140
|
+
}
|
|
2141
|
+
if (!parsed.data) {
|
|
2142
|
+
throw new Error(`DoorDash ${operationName} returned no data.`);
|
|
2143
|
+
}
|
|
2144
|
+
return parsed.data;
|
|
2145
|
+
}
|
|
2146
|
+
function asObject(value) {
|
|
2147
|
+
return value && typeof value === "object" ? value : {};
|
|
2148
|
+
}
|
|
2149
|
+
async function fetchExistingOrdersGraphql(params) {
|
|
2150
|
+
const requestedLimit = params.limit;
|
|
2151
|
+
const orders = [];
|
|
2152
|
+
let offset = 0;
|
|
2153
|
+
while (true) {
|
|
2154
|
+
const pageSize = requestedLimit == null ? 25 : Math.min(25, Math.max(requestedLimit - orders.length, 0));
|
|
2155
|
+
if (requestedLimit != null && pageSize === 0) {
|
|
2156
|
+
break;
|
|
2157
|
+
}
|
|
2158
|
+
const data = await session.graphql("getConsumerOrdersWithDetails", EXISTING_ORDERS_QUERY, {
|
|
2159
|
+
offset,
|
|
2160
|
+
limit: pageSize === 0 ? 25 : pageSize,
|
|
2161
|
+
includeCancelled: true,
|
|
2162
|
+
});
|
|
2163
|
+
const batch = parseExistingOrdersResponse(Array.isArray(data.getConsumerOrdersWithDetails) ? data.getConsumerOrdersWithDetails : []);
|
|
2164
|
+
if (batch.length === 0) {
|
|
2165
|
+
break;
|
|
2166
|
+
}
|
|
2167
|
+
orders.push(...batch);
|
|
2168
|
+
offset += batch.length;
|
|
2169
|
+
if (batch.length < (pageSize === 0 ? 25 : pageSize)) {
|
|
2170
|
+
break;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
return requestedLimit == null ? orders : orders.slice(0, requestedLimit);
|
|
2174
|
+
}
|
|
2175
|
+
function buildOrdersResult(input) {
|
|
2176
|
+
const filtered = input.activeOnly ? input.orders.filter((order) => order.isActive) : input.orders;
|
|
2177
|
+
const limited = input.requestedLimit == null ? filtered : filtered.slice(0, input.requestedLimit);
|
|
2178
|
+
return {
|
|
2179
|
+
success: true,
|
|
2180
|
+
source: input.source,
|
|
2181
|
+
warning: input.warning,
|
|
2182
|
+
count: limited.length,
|
|
2183
|
+
activeCount: limited.filter((order) => order.isActive).length,
|
|
2184
|
+
orders: limited,
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
function findExistingOrderByIdentifier(orders, orderId) {
|
|
2188
|
+
for (const field of ["orderUuid", "deliveryUuid", "id"]) {
|
|
2189
|
+
const match = orders.find((order) => order[field] === orderId);
|
|
2190
|
+
if (match) {
|
|
2191
|
+
return { matchedField: field, order: match };
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return null;
|
|
2195
|
+
}
|
|
2196
|
+
function resolveApolloCacheValue(cache, value) {
|
|
2197
|
+
if (!cache) {
|
|
2198
|
+
return value;
|
|
2199
|
+
}
|
|
2200
|
+
if (typeof value === "string" && value in cache) {
|
|
2201
|
+
return cache[value];
|
|
2202
|
+
}
|
|
2203
|
+
const object = asObject(value);
|
|
2204
|
+
if (typeof object.__ref === "string" && object.__ref in cache) {
|
|
2205
|
+
return cache[object.__ref];
|
|
2206
|
+
}
|
|
2207
|
+
return value;
|
|
2208
|
+
}
|
|
2209
|
+
function compareIsoDateDesc(left, right) {
|
|
2210
|
+
const leftValue = left ? Date.parse(left) : Number.NEGATIVE_INFINITY;
|
|
2211
|
+
const rightValue = right ? Date.parse(right) : Number.NEGATIVE_INFINITY;
|
|
2212
|
+
return rightValue - leftValue;
|
|
2213
|
+
}
|
|
2214
|
+
function isOrderHistoryChallengeError(error) {
|
|
2215
|
+
if (!(error instanceof Error)) {
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
return /non-JSON response|Checking if the site connection is secured|cf-mitigated|DoorDash getConsumerOrdersWithDetails returned HTTP 403/i.test(error.message);
|
|
2219
|
+
}
|
|
2220
|
+
function isRetryablePageEvaluateError(error) {
|
|
2221
|
+
if (!(error instanceof Error)) {
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
return /Execution context was destroyed|Cannot find context with specified id|Target page, context or browser has been closed/i.test(error.message);
|
|
2225
|
+
}
|
|
2226
|
+
function safeJsonParse(value) {
|
|
2227
|
+
if (!value) {
|
|
2228
|
+
return null;
|
|
2229
|
+
}
|
|
2230
|
+
try {
|
|
2231
|
+
return JSON.parse(value);
|
|
2232
|
+
}
|
|
2233
|
+
catch {
|
|
2234
|
+
return null;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
function extractStoreId(url) {
|
|
2238
|
+
if (!url) {
|
|
2239
|
+
return null;
|
|
2240
|
+
}
|
|
2241
|
+
const match = url.match(/\/(?:convenience\/)?store\/(\d+)(?:\/|\?|$)/);
|
|
2242
|
+
return match?.[1] ?? null;
|
|
2243
|
+
}
|
|
2244
|
+
function extractIdFromFacetId(value) {
|
|
2245
|
+
const match = value.match(/row\.store:(?:ad_)?(\d+):/);
|
|
2246
|
+
return match?.[1] ?? null;
|
|
2247
|
+
}
|
|
2248
|
+
function parseCuisineDescription(description) {
|
|
2249
|
+
if (!description) {
|
|
2250
|
+
return [];
|
|
2251
|
+
}
|
|
2252
|
+
return description
|
|
2253
|
+
.split("•")
|
|
2254
|
+
.map((part) => part.trim())
|
|
2255
|
+
.filter((part) => part.length > 0 && !/^\$+$/.test(part));
|
|
2256
|
+
}
|
|
2257
|
+
function dedupeBy(values, keyFn) {
|
|
2258
|
+
const seen = new Set();
|
|
2259
|
+
const deduped = [];
|
|
2260
|
+
for (const value of values) {
|
|
2261
|
+
const key = keyFn(value);
|
|
2262
|
+
if (seen.has(key)) {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
seen.add(key);
|
|
2266
|
+
deduped.push(value);
|
|
2267
|
+
}
|
|
2268
|
+
return deduped;
|
|
2269
|
+
}
|
|
2270
|
+
function truncate(value, length) {
|
|
2271
|
+
return value.length <= length ? value : `${value.slice(0, length)}...`;
|
|
2272
|
+
}
|
|
2273
|
+
async function ensureConfigDir() {
|
|
2274
|
+
await mkdir(dirname(getCookiesPath()), { recursive: true });
|
|
2275
|
+
}
|
|
2276
|
+
async function hasStorageState() {
|
|
2277
|
+
try {
|
|
2278
|
+
await readFile(getStorageStatePath(), "utf8");
|
|
2279
|
+
return true;
|
|
2280
|
+
}
|
|
2281
|
+
catch {
|
|
2282
|
+
return false;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
async function readStoredCookies() {
|
|
2286
|
+
try {
|
|
2287
|
+
const raw = await readFile(getCookiesPath(), "utf8");
|
|
2288
|
+
const parsed = JSON.parse(raw);
|
|
2289
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2290
|
+
}
|
|
2291
|
+
catch {
|
|
2292
|
+
return [];
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
export function getStorageStatePath() {
|
|
2296
|
+
return join(dirname(getCookiesPath()), "storage-state.json");
|
|
2297
|
+
}
|