food402 1.0.2 → 1.0.4-beta.1

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.
@@ -0,0 +1,945 @@
1
+ // shared/api.ts - TGO Yemek API Functions (token-parameterized for multi-user support)
2
+ const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36";
3
+ const API_BASE = "https://api.tgoapis.com";
4
+ const PAYMENT_API_BASE = "https://payment.tgoapps.com";
5
+ // UUID generator that works in both Node.js and Cloudflare Workers
6
+ function generateUUID() {
7
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
8
+ return crypto.randomUUID();
9
+ }
10
+ // Fallback for environments without crypto.randomUUID
11
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
12
+ const r = (Math.random() * 16) | 0;
13
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
14
+ return v.toString(16);
15
+ });
16
+ }
17
+ // Helper to create common headers
18
+ function createHeaders(token, contentType) {
19
+ const headers = {
20
+ "Accept": "application/json, text/plain, */*",
21
+ "Authorization": `Bearer ${token}`,
22
+ "User-Agent": USER_AGENT,
23
+ "Origin": "https://tgoyemek.com",
24
+ "x-correlationid": generateUUID(),
25
+ "pid": generateUUID(),
26
+ "sid": generateUUID(),
27
+ };
28
+ if (contentType) {
29
+ headers["Content-Type"] = contentType;
30
+ }
31
+ return headers;
32
+ }
33
+ // Helper to create payment headers
34
+ function createPaymentHeaders(token, correlationId, pid, sid) {
35
+ return {
36
+ "Accept": "application/json, text/plain, */*",
37
+ "Authorization": `Bearer ${token}`,
38
+ "Content-Type": "application/json",
39
+ "User-Agent": USER_AGENT,
40
+ "Origin": "https://tgoyemek.com",
41
+ "app-name": "TrendyolGo",
42
+ "x-applicationid": "1",
43
+ "x-channelid": "4",
44
+ "x-storefrontid": "1",
45
+ "x-features": "OPTIONAL_REBATE;MEAL_CART_ENABLED",
46
+ "x-supported-payment-options": "MULTINET;SODEXO;EDENRED;ON_DELIVERY;SETCARD",
47
+ "x-correlationid": correlationId || generateUUID(),
48
+ "pid": pid || generateUUID(),
49
+ "sid": sid || generateUUID(),
50
+ };
51
+ }
52
+ export async function getAddresses(token) {
53
+ const response = await fetch(`${API_BASE}/web-user-apimemberaddress-santral/addresses`, {
54
+ method: "GET",
55
+ headers: createHeaders(token),
56
+ });
57
+ if (!response.ok) {
58
+ throw new Error(`Failed to fetch addresses: ${response.status} ${response.statusText}`);
59
+ }
60
+ return response.json();
61
+ }
62
+ export async function getRestaurants(token, latitude, longitude, page = 1) {
63
+ const pageSize = 50;
64
+ const params = new URLSearchParams({
65
+ sortType: "RESTAURANT_SCORE",
66
+ minBasketPrice: "400",
67
+ openRestaurants: "true",
68
+ latitude,
69
+ longitude,
70
+ pageSize: pageSize.toString(),
71
+ page: page.toString(),
72
+ });
73
+ const response = await fetch(`${API_BASE}/web-discovery-apidiscovery-santral/restaurants/filters?${params}`, {
74
+ method: "GET",
75
+ headers: createHeaders(token),
76
+ });
77
+ if (!response.ok) {
78
+ throw new Error(`Failed to fetch restaurants: ${response.status} ${response.statusText}`);
79
+ }
80
+ const data = await response.json();
81
+ const restaurants = data.restaurants.map((r) => ({
82
+ id: r.id,
83
+ name: r.name,
84
+ kitchen: r.kitchen,
85
+ rating: r.rating,
86
+ ratingText: r.ratingText,
87
+ minBasketPrice: r.minBasketPrice,
88
+ averageDeliveryInterval: r.averageDeliveryInterval,
89
+ distance: r.location?.distance ?? 0,
90
+ neighborhoodName: r.location?.neighborhoodName ?? "",
91
+ isClosed: r.isClosed,
92
+ campaignText: r.campaignText,
93
+ }));
94
+ return {
95
+ restaurants,
96
+ totalCount: data.restaurantCount,
97
+ currentPage: page,
98
+ pageSize,
99
+ hasNextPage: !!data.links?.next?.href,
100
+ };
101
+ }
102
+ export async function getRestaurantMenu(token, restaurantId, latitude, longitude) {
103
+ const params = new URLSearchParams({ latitude, longitude });
104
+ const response = await fetch(`${API_BASE}/web-restaurant-apirestaurant-santral/restaurants/${restaurantId}?${params}`, {
105
+ method: "GET",
106
+ headers: createHeaders(token),
107
+ });
108
+ if (!response.ok) {
109
+ throw new Error(`Failed to fetch restaurant menu: ${response.status} ${response.statusText}`);
110
+ }
111
+ const data = await response.json();
112
+ const restaurant = data.restaurant;
113
+ const info = {
114
+ id: restaurant.info.id,
115
+ name: restaurant.info.name,
116
+ status: restaurant.info.status,
117
+ rating: restaurant.info.score?.overall ?? 0,
118
+ ratingText: restaurant.info.score?.ratingText ?? "",
119
+ workingHours: restaurant.info.workingHours,
120
+ deliveryTime: restaurant.info.deliveryInfo?.eta ?? "",
121
+ minOrderPrice: restaurant.info.deliveryInfo?.minPrice ?? 0,
122
+ };
123
+ let totalItems = 0;
124
+ const categories = restaurant.sections.map((section) => {
125
+ const items = section.products.map((product) => ({
126
+ id: product.id,
127
+ name: product.name,
128
+ description: product.description ?? "",
129
+ price: product.price?.salePrice ?? 0,
130
+ likePercentage: product.productScore?.likePercentageInfo,
131
+ }));
132
+ totalItems += items.length;
133
+ return {
134
+ name: section.name,
135
+ slug: section.slug,
136
+ items,
137
+ };
138
+ });
139
+ return {
140
+ info,
141
+ categories,
142
+ totalItems,
143
+ };
144
+ }
145
+ export async function getProductRecommendations(token, restaurantId, productIds) {
146
+ const response = await fetch(`${API_BASE}/web-discovery-apidiscovery-santral/recommendation/product`, {
147
+ method: "POST",
148
+ headers: createHeaders(token, "application/json"),
149
+ body: JSON.stringify({
150
+ restaurantId: restaurantId.toString(),
151
+ productIds: productIds.map((id) => id.toString()),
152
+ page: "PDP",
153
+ }),
154
+ });
155
+ if (!response.ok) {
156
+ throw new Error(`Failed to fetch product recommendations: ${response.status} ${response.statusText}`);
157
+ }
158
+ const data = await response.json();
159
+ let totalItems = 0;
160
+ const collections = (data.collections || []).map((collection) => {
161
+ const items = (collection.items || []).map((item) => ({
162
+ id: item.id,
163
+ name: item.name,
164
+ description: item.description,
165
+ price: item.sellingPrice ?? 0,
166
+ imageUrl: item.imageUrl ?? "",
167
+ }));
168
+ totalItems += items.length;
169
+ return {
170
+ name: collection.name,
171
+ items,
172
+ };
173
+ });
174
+ return {
175
+ collections,
176
+ totalItems,
177
+ };
178
+ }
179
+ export async function getProductDetails(token, restaurantId, productId, latitude, longitude) {
180
+ const params = new URLSearchParams({ latitude, longitude });
181
+ const response = await fetch(`${API_BASE}/web-restaurant-apirestaurant-santral/restaurants/${restaurantId}/products/${productId}?${params}`, {
182
+ method: "POST",
183
+ headers: createHeaders(token, "application/json"),
184
+ body: JSON.stringify({}),
185
+ });
186
+ if (!response.ok) {
187
+ throw new Error(`Failed to fetch product details: ${response.status} ${response.statusText}`);
188
+ }
189
+ const data = await response.json();
190
+ const components = (data.components || []).map((comp) => ({
191
+ type: comp.type,
192
+ title: comp.title,
193
+ description: comp.description,
194
+ modifierGroupId: comp.modifierGroupId,
195
+ options: (comp.options || []).map((opt) => ({
196
+ id: opt.optionId,
197
+ name: opt.title,
198
+ price: opt.price?.salePrice ?? 0,
199
+ selected: opt.selected ?? false,
200
+ isPopular: opt.badges?.some((b) => b.type === "POPULAR_OPTION") ?? false,
201
+ })),
202
+ isSingleChoice: comp.isSingleChoice ?? false,
203
+ minSelections: comp.min ?? 0,
204
+ maxSelections: comp.max ?? 0,
205
+ }));
206
+ return {
207
+ restaurantId: data.restaurantId,
208
+ restaurantName: data.restaurantName,
209
+ productId: data.productId,
210
+ productName: data.productName,
211
+ description: data.productDescription ?? "",
212
+ imageUrl: data.productImage ?? "",
213
+ price: data.price?.salePrice ?? 0,
214
+ maxQuantity: data.maxQuantity ?? 50,
215
+ components,
216
+ };
217
+ }
218
+ export async function setShippingAddress(token, request) {
219
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/shipping`, {
220
+ method: "POST",
221
+ headers: createHeaders(token, "application/json"),
222
+ body: JSON.stringify(request),
223
+ });
224
+ if (!response.ok) {
225
+ throw new Error(`Failed to set shipping address: ${response.status} ${response.statusText}`);
226
+ }
227
+ }
228
+ export async function addToBasket(token, request) {
229
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/carts/items`, {
230
+ method: "POST",
231
+ headers: createHeaders(token, "application/json"),
232
+ body: JSON.stringify({
233
+ storeId: request.storeId,
234
+ items: request.items,
235
+ isFlashSale: false,
236
+ storePickup: false,
237
+ latitude: request.latitude,
238
+ longitude: request.longitude,
239
+ }),
240
+ });
241
+ if (!response.ok) {
242
+ throw new Error(`Failed to add to basket: ${response.status} ${response.statusText}`);
243
+ }
244
+ const data = await response.json();
245
+ const storeData = data.groupedProducts?.[0]?.store;
246
+ const store = {
247
+ id: storeData?.id ?? request.storeId,
248
+ name: storeData?.name ?? "",
249
+ imageUrl: storeData?.imageUrl ?? "",
250
+ rating: storeData?.rating ?? 0,
251
+ averageDeliveryInterval: storeData?.averageDeliveryInterval ?? "",
252
+ minAmount: storeData?.minAmount ?? 0,
253
+ };
254
+ const products = (data.groupedProducts?.[0]?.products || []).map((p) => ({
255
+ productId: p.productId,
256
+ itemId: p.itemId,
257
+ name: p.name,
258
+ quantity: p.quantity,
259
+ salePrice: p.salePrice,
260
+ description: p.description ?? "",
261
+ }));
262
+ const summary = (data.summary || []).map((s) => ({
263
+ title: s.title,
264
+ amount: s.amount,
265
+ isPromotion: s.isPromotion ?? false,
266
+ }));
267
+ return {
268
+ store,
269
+ products,
270
+ summary,
271
+ totalProductCount: data.totalProductCount ?? 0,
272
+ totalProductPrice: data.totalProductPrice ?? 0,
273
+ totalProductPriceDiscounted: data.totalProductPriceDiscounted ?? 0,
274
+ totalPrice: data.totalPrice ?? 0,
275
+ deliveryPrice: data.deliveryPrice ?? 0,
276
+ };
277
+ }
278
+ export async function getBasket(token) {
279
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/carts`, {
280
+ method: "GET",
281
+ headers: createHeaders(token),
282
+ });
283
+ if (!response.ok) {
284
+ throw new Error(`Failed to get basket: ${response.status} ${response.statusText}`);
285
+ }
286
+ const data = await response.json();
287
+ const storeGroups = (data.groupedProducts || []).map((group) => ({
288
+ store: {
289
+ id: group.store?.id ?? 0,
290
+ name: group.store?.name ?? "",
291
+ imageUrl: group.store?.imageUrl ?? "",
292
+ rating: group.store?.rating ?? 0,
293
+ averageDeliveryInterval: group.store?.averageDeliveryInterval ?? "",
294
+ minAmount: group.store?.minAmount ?? 0,
295
+ },
296
+ products: (group.products || []).map((p) => ({
297
+ productId: p.productId,
298
+ itemId: p.itemId,
299
+ name: p.name,
300
+ quantity: p.quantity,
301
+ salePrice: p.salePrice,
302
+ description: p.description ?? "",
303
+ marketPrice: p.marketPrice ?? 0,
304
+ modifierProducts: (p.modifierProducts || []).map((m) => ({
305
+ productId: m.productId,
306
+ modifierGroupId: m.modifierGroupId,
307
+ name: m.name,
308
+ price: m.price,
309
+ })),
310
+ ingredientExcludes: (p.ingredientOption?.excludes || []).map((e) => ({
311
+ id: e.id,
312
+ name: e.name,
313
+ })),
314
+ })),
315
+ }));
316
+ const summary = (data.summary || []).map((s) => ({
317
+ title: s.title,
318
+ amount: s.amount,
319
+ isPromotion: s.isPromotion ?? false,
320
+ }));
321
+ return {
322
+ storeGroups,
323
+ summary,
324
+ totalProductCount: data.totalProductCount ?? 0,
325
+ totalProductPrice: data.totalProductPrice ?? 0,
326
+ totalProductPriceDiscounted: data.totalProductPriceDiscounted ?? 0,
327
+ totalPrice: data.totalPrice ?? 0,
328
+ deliveryPrice: data.deliveryPrice ?? 0,
329
+ isEmpty: (data.totalProductCount ?? 0) === 0,
330
+ };
331
+ }
332
+ export async function removeFromBasket(token, itemId) {
333
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/carts/items/${itemId}`, {
334
+ method: "DELETE",
335
+ headers: createHeaders(token),
336
+ });
337
+ if (!response.ok) {
338
+ throw new Error(`Failed to remove from basket: ${response.status} ${response.statusText}`);
339
+ }
340
+ const data = await response.json();
341
+ const storeGroups = (data.groupedProducts || []).map((group) => ({
342
+ store: {
343
+ id: group.store?.id ?? 0,
344
+ name: group.store?.name ?? "",
345
+ imageUrl: group.store?.imageUrl ?? "",
346
+ rating: group.store?.rating ?? 0,
347
+ averageDeliveryInterval: group.store?.averageDeliveryInterval ?? "",
348
+ minAmount: group.store?.minAmount ?? 0,
349
+ },
350
+ products: (group.products || []).map((p) => ({
351
+ productId: p.productId,
352
+ itemId: p.itemId,
353
+ name: p.name,
354
+ quantity: p.quantity,
355
+ salePrice: p.salePrice,
356
+ description: p.description ?? "",
357
+ marketPrice: p.marketPrice ?? 0,
358
+ modifierProducts: (p.modifierProducts || []).map((m) => ({
359
+ productId: m.productId,
360
+ modifierGroupId: m.modifierGroupId,
361
+ name: m.name,
362
+ price: m.price,
363
+ })),
364
+ ingredientExcludes: (p.ingredientOption?.excludes || []).map((e) => ({
365
+ id: e.id,
366
+ name: e.name,
367
+ })),
368
+ })),
369
+ }));
370
+ const summary = (data.summary || []).map((s) => ({
371
+ title: s.title,
372
+ amount: s.amount,
373
+ isPromotion: s.isPromotion ?? false,
374
+ }));
375
+ return {
376
+ storeGroups,
377
+ summary,
378
+ totalProductCount: data.totalProductCount ?? 0,
379
+ totalProductPrice: data.totalProductPrice ?? 0,
380
+ totalProductPriceDiscounted: data.totalProductPriceDiscounted ?? 0,
381
+ totalPrice: data.totalPrice ?? 0,
382
+ deliveryPrice: data.deliveryPrice ?? 0,
383
+ isEmpty: (data.totalProductCount ?? 0) === 0,
384
+ };
385
+ }
386
+ export async function clearBasket(token) {
387
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/carts`, {
388
+ method: "DELETE",
389
+ headers: createHeaders(token),
390
+ });
391
+ if (!response.ok) {
392
+ throw new Error(`Failed to clear basket: ${response.status} ${response.statusText}`);
393
+ }
394
+ }
395
+ export async function getCities(token) {
396
+ const response = await fetch(`${API_BASE}/web-user-apimemberaddress-santral/cities`, {
397
+ method: "GET",
398
+ headers: createHeaders(token),
399
+ });
400
+ if (!response.ok) {
401
+ throw new Error(`Failed to fetch cities: ${response.status} ${response.statusText}`);
402
+ }
403
+ const data = await response.json();
404
+ const cities = (data.cities || []).map((c) => ({
405
+ id: c.id,
406
+ code: c.code,
407
+ name: c.name,
408
+ }));
409
+ return {
410
+ cities,
411
+ count: cities.length,
412
+ };
413
+ }
414
+ export async function getDistricts(token, cityId) {
415
+ const response = await fetch(`${API_BASE}/web-user-apimemberaddress-santral/cities/${cityId}/districts`, {
416
+ method: "GET",
417
+ headers: createHeaders(token),
418
+ });
419
+ if (!response.ok) {
420
+ throw new Error(`Failed to fetch districts: ${response.status} ${response.statusText}`);
421
+ }
422
+ const data = await response.json();
423
+ const districts = (data.districts || []).map((d) => ({
424
+ id: d.id,
425
+ name: d.name,
426
+ }));
427
+ return {
428
+ districts,
429
+ count: districts.length,
430
+ cityId,
431
+ };
432
+ }
433
+ export async function getNeighborhoods(token, districtId) {
434
+ const response = await fetch(`${API_BASE}/web-user-apimemberaddress-santral/districts/${districtId}/neighborhoods`, {
435
+ method: "GET",
436
+ headers: createHeaders(token),
437
+ });
438
+ if (!response.ok) {
439
+ throw new Error(`Failed to fetch neighborhoods: ${response.status} ${response.statusText}`);
440
+ }
441
+ const data = await response.json();
442
+ const neighborhoods = (data.neighborhoods || []).map((n) => ({
443
+ id: n.id,
444
+ name: n.name,
445
+ }));
446
+ return {
447
+ neighborhoods,
448
+ count: neighborhoods.length,
449
+ districtId,
450
+ };
451
+ }
452
+ export async function addAddress(token, request) {
453
+ const payload = {
454
+ name: request.name,
455
+ surname: request.surname,
456
+ phone: request.phone,
457
+ apartmentNumber: request.apartmentNumber ?? "",
458
+ floor: request.floor ?? "",
459
+ doorNumber: request.doorNumber ?? "",
460
+ addressName: request.addressName,
461
+ addressDescription: request.addressDescription ?? "",
462
+ addressLine: request.addressLine,
463
+ cityId: request.cityId,
464
+ districtId: request.districtId,
465
+ neighborhoodId: request.neighborhoodId,
466
+ latitude: request.latitude,
467
+ longitude: request.longitude,
468
+ countryCode: request.countryCode ?? "TR",
469
+ elevatorAvailable: request.elevatorAvailable ?? false,
470
+ };
471
+ const response = await fetch(`${API_BASE}/web-user-apimemberaddress-santral/addresses`, {
472
+ method: "POST",
473
+ headers: createHeaders(token, "application/json"),
474
+ body: JSON.stringify(payload),
475
+ });
476
+ if (response.status === 429) {
477
+ return {
478
+ success: false,
479
+ requiresOtp: true,
480
+ message: "OTP verification required. Please add this address through the TGO Yemek website.",
481
+ };
482
+ }
483
+ if (!response.ok) {
484
+ throw new Error(`Failed to add address: ${response.status} ${response.statusText}`);
485
+ }
486
+ const data = await response.json();
487
+ const address = {
488
+ id: data.id,
489
+ name: data.name,
490
+ surname: data.surname,
491
+ phone: data.phone,
492
+ countryPhoneCode: data.countryPhoneCode ?? "+90",
493
+ addressLine: data.addressLine,
494
+ addressName: data.addressName,
495
+ postalCode: data.postalCode ?? "",
496
+ cityId: data.cityId,
497
+ cityName: data.cityName ?? "",
498
+ districtId: data.districtId,
499
+ districtName: data.districtName ?? "",
500
+ neighborhoodId: data.neighborhoodId,
501
+ neighborhoodName: data.neighborhoodName ?? "",
502
+ latitude: data.latitude,
503
+ longitude: data.longitude,
504
+ addressDescription: data.addressDescription ?? "",
505
+ apartmentNumber: data.apartmentNumber ?? "",
506
+ floor: data.floor ?? "",
507
+ doorNumber: data.doorNumber ?? "",
508
+ addressType: data.addressType ?? "HOME",
509
+ elevatorAvailable: data.elevatorAvailable ?? false,
510
+ };
511
+ return {
512
+ success: true,
513
+ address,
514
+ message: "Address added successfully",
515
+ };
516
+ }
517
+ export async function updateCustomerNote(token, request) {
518
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/carts/customerNote`, {
519
+ method: "PUT",
520
+ headers: createHeaders(token, "application/json"),
521
+ body: JSON.stringify({
522
+ customerNote: request.customerNote,
523
+ noServiceWare: request.noServiceWare,
524
+ contactlessDelivery: request.contactlessDelivery,
525
+ dontRingBell: request.dontRingBell,
526
+ }),
527
+ });
528
+ if (!response.ok) {
529
+ throw new Error(`Failed to update customer note: ${response.status} ${response.statusText}`);
530
+ }
531
+ }
532
+ export async function getSavedCards(token) {
533
+ const response = await fetch(`${PAYMENT_API_BASE}/v2/cards/`, {
534
+ method: "GET",
535
+ headers: createPaymentHeaders(token),
536
+ });
537
+ if (!response.ok) {
538
+ throw new Error(`Failed to fetch saved cards: ${response.status} ${response.statusText}`);
539
+ }
540
+ const data = await response.json();
541
+ const cardsData = data.json?.cards || data.cards || [];
542
+ const cards = cardsData.map((c) => ({
543
+ cardId: c.cardId,
544
+ name: c.name ?? "",
545
+ maskedCardNumber: c.maskedCardNumber ?? "",
546
+ cardTypeName: c.cardTypeName ?? "",
547
+ bankName: c.bankName ?? "",
548
+ isDebitCard: c.isDebitCard ?? false,
549
+ cvvRequired: c.cvvRequired ?? false,
550
+ cardNetwork: c.cardNetwork ?? "",
551
+ }));
552
+ if (cards.length === 0) {
553
+ return {
554
+ cards: [],
555
+ hasCards: false,
556
+ message: "No saved cards. Please add a payment method at tgoyemek.com",
557
+ };
558
+ }
559
+ return {
560
+ cards,
561
+ hasCards: true,
562
+ };
563
+ }
564
+ export async function getCheckoutReady(token) {
565
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/carts?cartContext=payment&limitPromoMbs=false`, {
566
+ method: "GET",
567
+ headers: createHeaders(token),
568
+ });
569
+ if (response.status === 400) {
570
+ return {
571
+ ready: false,
572
+ store: {
573
+ id: 0,
574
+ name: "",
575
+ imageUrl: "",
576
+ rating: 0,
577
+ averageDeliveryInterval: "",
578
+ minAmount: 0,
579
+ },
580
+ products: [],
581
+ summary: [],
582
+ totalPrice: 0,
583
+ deliveryPrice: 0,
584
+ warnings: ["Cart is empty. Add items before checkout."],
585
+ };
586
+ }
587
+ if (!response.ok) {
588
+ throw new Error(`Failed to get checkout ready: ${response.status} ${response.statusText}`);
589
+ }
590
+ const data = await response.json();
591
+ const warnings = [];
592
+ if (data.warnings) {
593
+ warnings.push(...data.warnings.map((w) => w.message || String(w)));
594
+ }
595
+ if ((data.totalProductCount ?? 0) === 0) {
596
+ return {
597
+ ready: false,
598
+ store: {
599
+ id: 0,
600
+ name: "",
601
+ imageUrl: "",
602
+ rating: 0,
603
+ averageDeliveryInterval: "",
604
+ minAmount: 0,
605
+ },
606
+ products: [],
607
+ summary: [],
608
+ totalPrice: 0,
609
+ deliveryPrice: 0,
610
+ warnings: ["Cart is empty. Add items before checkout."],
611
+ };
612
+ }
613
+ const group = data.groupedProducts?.[0];
614
+ const store = {
615
+ id: group?.store?.id ?? 0,
616
+ name: group?.store?.name ?? "",
617
+ imageUrl: group?.store?.imageUrl ?? "",
618
+ rating: group?.store?.rating ?? 0,
619
+ averageDeliveryInterval: group?.store?.averageDeliveryInterval ?? "",
620
+ minAmount: group?.store?.minAmount ?? 0,
621
+ };
622
+ const products = (group?.products || []).map((p) => ({
623
+ productId: p.productId,
624
+ itemId: p.itemId,
625
+ name: p.name,
626
+ quantity: p.quantity,
627
+ salePrice: p.salePrice,
628
+ description: p.description ?? "",
629
+ marketPrice: p.marketPrice ?? 0,
630
+ modifierProducts: (p.modifierProducts || []).map((m) => ({
631
+ productId: m.productId,
632
+ modifierGroupId: m.modifierGroupId,
633
+ name: m.name,
634
+ price: m.price,
635
+ })),
636
+ ingredientExcludes: (p.ingredientOption?.excludes || []).map((e) => ({
637
+ id: e.id,
638
+ name: e.name,
639
+ })),
640
+ }));
641
+ const summary = (data.summary || []).map((s) => ({
642
+ title: s.title,
643
+ amount: s.amount,
644
+ isPromotion: s.isPromotion ?? false,
645
+ }));
646
+ const minAmount = store.minAmount || 0;
647
+ const totalPrice = data.totalPrice ?? 0;
648
+ if (minAmount > 0 && totalPrice < minAmount) {
649
+ warnings.push(`Minimum order amount is ${minAmount} TL. Current total: ${totalPrice} TL`);
650
+ }
651
+ return {
652
+ ready: warnings.length === 0,
653
+ store,
654
+ products,
655
+ summary,
656
+ totalPrice: data.totalPrice ?? 0,
657
+ deliveryPrice: data.deliveryPrice ?? 0,
658
+ warnings,
659
+ };
660
+ }
661
+ export async function placeOrder(token, cardId) {
662
+ // First, get the saved cards to find the bin code for this card
663
+ const cardsResponse = await getSavedCards(token);
664
+ const card = cardsResponse.cards.find((c) => c.cardId === cardId);
665
+ if (!card) {
666
+ return {
667
+ success: false,
668
+ message: `Card with ID ${cardId} not found. Use get_saved_cards to see available cards.`,
669
+ };
670
+ }
671
+ // Extract bin code from masked card number (first 6 digits + **)
672
+ const binCode = card.maskedCardNumber.substring(0, 6) + "**";
673
+ // Use the same session IDs across all payment-related calls
674
+ const correlationId = generateUUID();
675
+ const pid = generateUUID();
676
+ const sid = generateUUID();
677
+ const paymentHeaders = createPaymentHeaders(token, correlationId, pid, sid);
678
+ // Step 1: Initialize cart state in payment system
679
+ const checkoutResponse = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/carts?cartContext=payment&limitPromoMbs=false`, { method: "GET", headers: paymentHeaders });
680
+ if (!checkoutResponse.ok) {
681
+ return {
682
+ success: false,
683
+ message: `Failed to initialize checkout: ${checkoutResponse.status} ${checkoutResponse.statusText}`,
684
+ };
685
+ }
686
+ // Step 2: Select payment method
687
+ const optionsResponse = await fetch(`${PAYMENT_API_BASE}/v3/payment/options`, {
688
+ method: "POST",
689
+ headers: paymentHeaders,
690
+ body: JSON.stringify({
691
+ paymentType: "payWithCard",
692
+ data: {
693
+ savedCardId: cardId,
694
+ binCode: binCode,
695
+ installmentId: 0,
696
+ reward: null,
697
+ installmentPostponingSelected: false,
698
+ },
699
+ }),
700
+ });
701
+ if (!optionsResponse.ok) {
702
+ return {
703
+ success: false,
704
+ message: `Failed to select payment method: ${optionsResponse.status} ${optionsResponse.statusText}`,
705
+ };
706
+ }
707
+ // Step 3: Place the order with 3D Secure
708
+ const response = await fetch(`${PAYMENT_API_BASE}/v2/payment/pay`, {
709
+ method: "POST",
710
+ headers: paymentHeaders,
711
+ body: JSON.stringify({
712
+ customerSelectedThreeD: false,
713
+ paymentOptions: [
714
+ {
715
+ name: "payWithCard",
716
+ cardNo: "",
717
+ customerSelectedThreeD: false,
718
+ },
719
+ ],
720
+ callbackUrl: "https://tgoyemek.com/odeme",
721
+ }),
722
+ });
723
+ if (!response.ok) {
724
+ const errorText = await response.text();
725
+ if (response.status === 400 || response.status === 403) {
726
+ try {
727
+ const errorData = JSON.parse(errorText);
728
+ if (errorData.redirectUrl ||
729
+ errorData.requires3DSecure ||
730
+ errorData.threeDSecureUrl ||
731
+ errorData.htmlContent ||
732
+ errorData.json?.content) {
733
+ return {
734
+ success: false,
735
+ requires3DSecure: true,
736
+ redirectUrl: errorData.redirectUrl || errorData.threeDSecureUrl,
737
+ htmlContent: errorData.htmlContent || errorData.json?.content,
738
+ message: "3D Secure verification required. Complete payment in browser.",
739
+ };
740
+ }
741
+ }
742
+ catch {
743
+ // Not JSON, continue with generic error
744
+ }
745
+ }
746
+ throw new Error(`Failed to place order: ${response.status} ${response.statusText}`);
747
+ }
748
+ const data = await response.json();
749
+ // Check if 3D Secure HTML content is returned
750
+ if (data.json?.content) {
751
+ const formMatch = data.json.content.match(/action="([^"]+)"/);
752
+ const redirectUrl = formMatch ? formMatch[1] : undefined;
753
+ return {
754
+ success: false,
755
+ requires3DSecure: true,
756
+ redirectUrl,
757
+ htmlContent: data.json.content,
758
+ message: "3D Secure verification required. Complete payment in browser.",
759
+ };
760
+ }
761
+ if (data.requires3DSecure || data.redirectUrl || data.threeDSecureUrl || data.htmlContent) {
762
+ return {
763
+ success: false,
764
+ requires3DSecure: true,
765
+ redirectUrl: data.redirectUrl || data.threeDSecureUrl,
766
+ htmlContent: data.htmlContent,
767
+ message: "3D Secure verification required. Complete payment in browser.",
768
+ };
769
+ }
770
+ return {
771
+ success: true,
772
+ orderId: data.orderId || data.orderNumber || data.id,
773
+ message: "Order placed successfully!",
774
+ };
775
+ }
776
+ export async function getOrders(token, page = 1) {
777
+ const pageSize = 50;
778
+ const params = new URLSearchParams({
779
+ page: page.toString(),
780
+ pageSize: pageSize.toString(),
781
+ });
782
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/orders?${params}`, {
783
+ method: "GET",
784
+ headers: createHeaders(token),
785
+ });
786
+ if (!response.ok) {
787
+ throw new Error(`Failed to fetch orders: ${response.status} ${response.statusText}`);
788
+ }
789
+ const data = await response.json();
790
+ const orders = (data.orders || []).map((o) => ({
791
+ id: o.id,
792
+ orderDate: o.orderDate ?? "",
793
+ store: {
794
+ id: o.store?.id ?? 0,
795
+ name: o.store?.name ?? "",
796
+ },
797
+ status: {
798
+ status: o.status?.status ?? "",
799
+ statusText: o.status?.statusText ?? "",
800
+ statusColor: o.status?.statusColor ?? "",
801
+ },
802
+ price: {
803
+ totalPrice: o.price?.totalPrice ?? 0,
804
+ totalPriceText: o.price?.totalPriceText ?? "",
805
+ refundedPrice: o.price?.refundedPrice ?? 0,
806
+ cancelledPrice: o.price?.cancelledPrice ?? 0,
807
+ totalDeliveryPrice: o.price?.totalDeliveryPrice ?? 0,
808
+ totalServicePrice: o.price?.totalServicePrice ?? 0,
809
+ },
810
+ productSummary: o.product?.name ?? "",
811
+ products: (o.productList || []).map((p) => ({
812
+ productId: p.productId,
813
+ name: p.name,
814
+ imageUrl: p.imageUrl ?? "",
815
+ })),
816
+ isReady: o.isReady ?? false,
817
+ }));
818
+ return {
819
+ orders,
820
+ pagination: {
821
+ currentPage: data.pagination?.currentPage ?? page,
822
+ pageSize: data.pagination?.pageSize ?? pageSize,
823
+ totalCount: data.pagination?.totalCount ?? 0,
824
+ hasNext: data.pagination?.hasNext ?? false,
825
+ },
826
+ };
827
+ }
828
+ export async function getOrderDetail(token, orderId) {
829
+ const params = new URLSearchParams({
830
+ orderId,
831
+ });
832
+ const response = await fetch(`${API_BASE}/web-checkout-apicheckout-santral/orders/detail?${params}`, {
833
+ method: "GET",
834
+ headers: createHeaders(token),
835
+ });
836
+ if (!response.ok) {
837
+ throw new Error(`Failed to fetch order detail: ${response.status} ${response.statusText}`);
838
+ }
839
+ const data = await response.json();
840
+ const shipment = data.shipment;
841
+ const shipmentSummary = shipment?.summary;
842
+ const shipmentItem = shipment?.items?.[0];
843
+ const statusSteps = (shipmentItem?.state?.statuses || []).map((s) => ({
844
+ status: s.status ?? "",
845
+ statusText: s.statusText ?? "",
846
+ }));
847
+ const products = (shipmentItem?.products || []).map((p) => ({
848
+ name: p.name ?? "",
849
+ imageUrl: p.imageUrl ?? "",
850
+ salePrice: p.salePrice ?? 0,
851
+ salePriceText: p.salePriceText ?? "",
852
+ quantity: p.quantity ?? 1,
853
+ description: p.description ?? "",
854
+ }));
855
+ const addr = data.deliveryAddress;
856
+ const summaryPrice = data.summary?.price;
857
+ return {
858
+ orderId: data.summary?.orderId ?? orderId,
859
+ orderNumber: data.summary?.orderNumber ?? "",
860
+ orderDate: data.summary?.orderDate ?? "",
861
+ customerNote: data.summary?.customerNote ?? "",
862
+ store: {
863
+ id: parseInt(shipmentSummary?.store?.id, 10) || 0,
864
+ name: shipmentSummary?.store?.name ?? "",
865
+ },
866
+ eta: shipmentSummary?.eta ?? "",
867
+ deliveredDate: shipmentSummary?.deliveredDate ?? "",
868
+ status: {
869
+ status: shipmentItem?.status?.status ?? "",
870
+ statusText: shipmentItem?.status?.statusText ?? "",
871
+ statusColor: shipmentItem?.status?.statusColor ?? "",
872
+ },
873
+ statusSteps,
874
+ products,
875
+ price: {
876
+ totalPrice: summaryPrice?.totalPrice ?? 0,
877
+ totalPriceText: summaryPrice?.totalPriceText ?? "",
878
+ refundedPrice: summaryPrice?.refundedPrice ?? 0,
879
+ cancelledPrice: summaryPrice?.cancelledPrice ?? 0,
880
+ totalDeliveryPrice: summaryPrice?.totalDeliveryPrice ?? 0,
881
+ totalServicePrice: summaryPrice?.totalServicePrice ?? 0,
882
+ },
883
+ paymentDescription: data.paymentInfo?.paymentDescription ?? "",
884
+ deliveryAddress: {
885
+ name: addr?.name ?? "",
886
+ address: addr?.address ?? "",
887
+ districtCity: addr?.districtCity ?? "",
888
+ phoneNumber: addr?.phoneNumber ?? "",
889
+ },
890
+ };
891
+ }
892
+ export async function searchRestaurants(token, searchQuery, latitude, longitude, page = 1) {
893
+ const pageSize = 50;
894
+ const params = new URLSearchParams({
895
+ searchQuery,
896
+ latitude,
897
+ longitude,
898
+ pageSize: pageSize.toString(),
899
+ page: page.toString(),
900
+ });
901
+ const response = await fetch(`${API_BASE}/web-restaurant-apirestaurant-santral/restaurants/in/search?${params}`, {
902
+ method: "GET",
903
+ headers: createHeaders(token),
904
+ });
905
+ if (!response.ok) {
906
+ throw new Error(`Failed to search restaurants: ${response.status} ${response.statusText}`);
907
+ }
908
+ const data = await response.json();
909
+ const restaurants = (data.restaurants || []).map((r) => {
910
+ const isClosed = r.isClosed ?? false;
911
+ return {
912
+ id: r.id,
913
+ name: r.name,
914
+ kitchen: r.kitchen ?? "",
915
+ rating: r.rating ?? 0,
916
+ ratingText: r.ratingText ?? "",
917
+ minBasketPrice: r.minBasketPrice ?? 0,
918
+ averageDeliveryInterval: r.averageDeliveryInterval ?? "",
919
+ distance: r.location?.distance ?? 0,
920
+ neighborhoodName: r.location?.neighborhoodName ?? "",
921
+ isClosed,
922
+ campaignText: r.campaignText,
923
+ products: (r.products || []).map((p) => ({
924
+ id: p.id,
925
+ name: p.name,
926
+ description: p.description,
927
+ price: p.price?.salePrice ?? p.price ?? 0,
928
+ imageUrl: p.imageUrl,
929
+ })),
930
+ ...(isClosed && {
931
+ warning: "This restaurant is currently closed. Do not proceed with ordering from this restaurant.",
932
+ }),
933
+ };
934
+ });
935
+ return {
936
+ restaurants,
937
+ totalCount: data.restaurantCount ?? 0,
938
+ currentPage: page,
939
+ pageSize,
940
+ hasNextPage: !!data.links?.next?.href,
941
+ searchQuery: data.searchQuery ?? searchQuery,
942
+ };
943
+ }
944
+ // Re-export types for convenience
945
+ export * from "./types.js";