scripter-x 1.0.28 → 1.0.29
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/package.json +1 -1
- package/src/commands.js +79 -16
- package/src/index.js +1 -0
- package/src/providers/zeptoCoupon.js +241 -0
- package/src/worker.js +15 -3
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as tp from './providers/tempotp.js';
|
|
|
8
8
|
import * as oc from './providers/otpcart.js';
|
|
9
9
|
import * as kuku from './providers/kuku.js';
|
|
10
10
|
import * as zepto from './providers/zepto.js';
|
|
11
|
+
import { checkCouponTier } from './providers/zeptoCoupon.js';
|
|
11
12
|
import { homedir } from 'node:os';
|
|
12
13
|
import { join, dirname } from 'node:path';
|
|
13
14
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
@@ -288,12 +289,44 @@ export async function recheck(io, _api, args = {}) {
|
|
|
288
289
|
// code → we verify, build the ZAUTH1 envelope, and write {phone}-{timestamp}.txt.
|
|
289
290
|
// LOOPS: after each one it asks for the next number — keep going until the user
|
|
290
291
|
// presses Esc (or double Ctrl+C). Runs entirely on your IP.
|
|
292
|
+
// Resolve the dir + tier-file path for a Zepto envelope. tier ∈ rs50|rs75|rs100|normal|unchecked|''.
|
|
293
|
+
function zeptoOutPath(args, number, tier) {
|
|
294
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
295
|
+
const suffix = tier ? `-${tier}` : '';
|
|
296
|
+
let baseDir;
|
|
297
|
+
if (args.out) {
|
|
298
|
+
const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
|
|
299
|
+
// if --out is an explicit file, honor it (no tier split); else treat as dir
|
|
300
|
+
if (/\.(txt|json)$/i.test(expanded) && !tier) return expanded;
|
|
301
|
+
baseDir = /\.(txt|json)$/i.test(expanded) ? dirname(expanded) : expanded;
|
|
302
|
+
} else {
|
|
303
|
+
const downloads = join(homedir(), 'Downloads');
|
|
304
|
+
baseDir = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto');
|
|
305
|
+
}
|
|
306
|
+
// tier accounts go into per-tier subfolders so the 4 types are cleanly separated
|
|
307
|
+
const dir = tier ? join(baseDir, tier) : baseDir;
|
|
308
|
+
mkdirSync(dir, { recursive: true });
|
|
309
|
+
return join(dir, `${number}-${stamp}.txt`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Append one envelope to the right tier file/dir and return the path written.
|
|
313
|
+
function saveZeptoEnvelope(args, number, envelope, tier) {
|
|
314
|
+
const dest = zeptoOutPath(args, number, tier);
|
|
315
|
+
writeFileSync(dest, envelope + '\n');
|
|
316
|
+
return dest;
|
|
317
|
+
}
|
|
318
|
+
|
|
291
319
|
export async function zeptoCmd(io, api, args = {}) {
|
|
292
320
|
const mode = args.manual ? 'manual' : (args.auto ? 'auto' : (await io.select('Choose Zepto login flow', [
|
|
293
321
|
{ label: 'Auto', value: 'auto', description: 'Headless extraction via OTPCart (Recommended)' },
|
|
294
322
|
{ label: 'Manual', value: 'manual', description: 'Enter phone + type OTP manually' }
|
|
295
323
|
])).value || 'auto');
|
|
296
324
|
|
|
325
|
+
// Optional: check the coupon tier (₹50/₹75/₹100/normal) per account + split into
|
|
326
|
+
// 4 files. Runs headlessly right after verify while the token is fresh.
|
|
327
|
+
const checkCoupon = args.checkCoupon != null ? args.checkCoupon
|
|
328
|
+
: await io.confirm('check coupon tier (₹50/₹75/₹100) + split into files?', false);
|
|
329
|
+
|
|
297
330
|
if (mode === 'manual') {
|
|
298
331
|
io.print(' ◉ Zepto — local OTP → ZAUTH1 envelope (Manual)');
|
|
299
332
|
io.print(' ◉ press esc to cancel', 'accent');
|
|
@@ -335,21 +368,18 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
335
368
|
// Build the ZAUTH1 envelope — the format the ZeptoAuthManager tool imports.
|
|
336
369
|
const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), args.cert);
|
|
337
370
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
} else {
|
|
346
|
-
const downloads = join(homedir(), 'Downloads');
|
|
347
|
-
dest = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto', fname);
|
|
371
|
+
// Optional coupon-tier check (token is fresh right now) → split into 4 files.
|
|
372
|
+
let tier = '';
|
|
373
|
+
if (checkCoupon) {
|
|
374
|
+
io.print(' ◉ checking coupon tier …');
|
|
375
|
+
const c = await checkCouponTier(session);
|
|
376
|
+
tier = c.tier; // rs50 | rs75 | rs100 | normal | unchecked
|
|
377
|
+
io.print(` ✓ coupon: ${c.label}`, c.tier === 'normal' || c.tier === 'unchecked' ? undefined : 'accent');
|
|
348
378
|
}
|
|
349
|
-
|
|
350
|
-
|
|
379
|
+
|
|
380
|
+
const dest = saveZeptoEnvelope(args, number, envelope, tier);
|
|
351
381
|
done++;
|
|
352
|
-
io.print(
|
|
382
|
+
io.print(` ✓ envelope saved${tier ? ` (${tier})` : ''} to:`);
|
|
353
383
|
io.print(` ${dest}`, 'accent');
|
|
354
384
|
io.print(' ◉ paste into ZeptoAuthManager → Import:');
|
|
355
385
|
io.print(` ${envelope}`, 'accent');
|
|
@@ -412,6 +442,7 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
412
442
|
requested: count,
|
|
413
443
|
concurrency,
|
|
414
444
|
certSha: cert,
|
|
445
|
+
checkCoupon,
|
|
415
446
|
onEvent: controller.handleEvent,
|
|
416
447
|
});
|
|
417
448
|
controller.stop = () => worker.stop();
|
|
@@ -450,11 +481,28 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
450
481
|
io.print(` ✓ saved ${saved.count} session(s) to 2 files:`);
|
|
451
482
|
io.print(` full → ${saved.fullPath}`, 'accent');
|
|
452
483
|
io.print(` zauth → ${saved.zauthPath}`, 'accent');
|
|
484
|
+
}
|
|
485
|
+
// If coupon-tier was checked, ALSO split the envelopes into per-tier files.
|
|
486
|
+
if (checkCoupon) {
|
|
487
|
+
const tiers = {};
|
|
488
|
+
for (const r of results) {
|
|
489
|
+
if (r.status !== 'success') continue;
|
|
490
|
+
const tier = r.coupon_tier || 'unchecked';
|
|
491
|
+
(tiers[tier] = tiers[tier] || []).push(r.envelope);
|
|
492
|
+
}
|
|
493
|
+
io.print(' ◉ coupon tiers:');
|
|
494
|
+
for (const tier of ['rs100', 'rs75', 'rs50', 'normal', 'unchecked']) {
|
|
495
|
+
if (!tiers[tier]?.length) continue;
|
|
496
|
+
const dir = zeptoTierDir(args, name);
|
|
497
|
+
const dest = join(dir, `${tier}.txt`);
|
|
498
|
+
mkdirSync(dir, { recursive: true });
|
|
499
|
+
writeFileSync(dest, tiers[tier].join('\n') + '\n');
|
|
500
|
+
io.print(` ${TIER_LABEL[tier]} (${tiers[tier].length}) → ${dest}`, 'accent');
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
453
503
|
io.print(' ◉ ZAUTH1 envelopes:');
|
|
454
504
|
for (const r of results) {
|
|
455
|
-
if (r.status === 'success') {
|
|
456
|
-
io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
|
|
457
|
-
}
|
|
505
|
+
if (r.status === 'success') io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
|
|
458
506
|
}
|
|
459
507
|
}
|
|
460
508
|
} else {
|
|
@@ -462,6 +510,21 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
462
510
|
}
|
|
463
511
|
}
|
|
464
512
|
|
|
513
|
+
const TIER_LABEL = { rs100: '🟢 ₹100', rs75: '🟢 ₹75', rs50: '🟢 ₹50', normal: '⚪ normal', unchecked: '❔ unchecked' };
|
|
514
|
+
|
|
515
|
+
// The directory the per-tier files go in for an auto run.
|
|
516
|
+
function zeptoTierDir(args, name) {
|
|
517
|
+
let baseDir;
|
|
518
|
+
if (args.out) {
|
|
519
|
+
const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
|
|
520
|
+
baseDir = /\.(txt|json)$/i.test(expanded) ? dirname(expanded) : expanded;
|
|
521
|
+
} else {
|
|
522
|
+
const downloads = join(homedir(), 'Downloads');
|
|
523
|
+
baseDir = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto');
|
|
524
|
+
}
|
|
525
|
+
return join(baseDir, name);
|
|
526
|
+
}
|
|
527
|
+
|
|
465
528
|
async function saveZeptoSessions(results, name, out) {
|
|
466
529
|
const { statSync } = await import('node:fs');
|
|
467
530
|
const ok = results.filter((r) => r.status === 'success');
|
package/src/index.js
CHANGED
|
@@ -119,6 +119,7 @@ async function main() {
|
|
|
119
119
|
target: flags.target, // run: 'flipkart' | 'zepto'
|
|
120
120
|
number: flags.number, otp: flags.otp, cert: flags.cert, // zepto: local OTP login → ZAUTH1 envelope
|
|
121
121
|
auto: flags.auto, manual: flags.manual,
|
|
122
|
+
checkCoupon: flags['check-coupon'] === true ? true : (flags['check-coupon'] === false ? false : null),
|
|
122
123
|
campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
|
|
123
124
|
};
|
|
124
125
|
try { await REGISTRY[fnName](cliIo, null, args); }
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Zepto coupon-tier checker — runs HEADLESSLY right after OTP verify, while the
|
|
2
|
+
// access token is fresh. Reads which new-user coupon (₹50 / ₹75 / ₹100 / none) an
|
|
3
|
+
// account is in, by building a cart over the MOV and reading the coupons fetch-list.
|
|
4
|
+
//
|
|
5
|
+
// Ported from zepto_coupon_checker/zeptoClient.js (CommonJS → ESM). Classification
|
|
6
|
+
// mirrors zepto_coupon_checker/idcheck.js: an explicit Z-CARTBOOST<N> code or a
|
|
7
|
+
// single {50,75,100} banner = that tier; a spread of 3+ amounts = the generic
|
|
8
|
+
// catalog (normal account, no unlocked coupon).
|
|
9
|
+
import https from 'node:https';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
const API = 'api.zepto.co.in';
|
|
13
|
+
const GW = 'api-gateway.zepto.co.in';
|
|
14
|
+
const SEARCH = 'user-search.zepto.co.in';
|
|
15
|
+
|
|
16
|
+
const uuid = () => crypto.randomUUID();
|
|
17
|
+
const randHex = (n) => crypto.randomBytes(n).toString('hex').slice(0, n);
|
|
18
|
+
|
|
19
|
+
const COMPATIBLE_COMPONENTS = 'EXTERNAL_COUPONS,BUNDLE,MULTI_SELLER_ENABLED,ROLLUPS,RE_PROMISE_ETA_ORDER_SCREEN_ENABLED,RECOMMENDED_COUPON_WIDGET,PHARMA_ENABLED,GAMIFICATION_ENABLED,DYNAMIC_FILTERS,HOMEPAGE_V2,COUPON_WIDGET_CART_REVAMP,AUTOSUGGESTION_PIP,NEW_ETA_BANNER,IS_DYNAMIC_NZS_SUPPORTED,ZEPTO_THREE,RECOMMENDED_COUPON_WIDGET,COUPON_UPSELLING_WIDGET,CART_BOX_MODEL_WIDGETS,NEW_BILL_INFO,COUPON_BOTTOM_STRIP,SUPERSTORE_V1,SUPER_SAVER:1,SUPER_SAVER_DYNAMIC_MOV_ENABLED,PROMO_CASH:2,GIFT_CARD,WISHLIST,CART_PERSISTENCE,CART_REDESIGN_ENABLED,CART_LOCKING:1,NEW_WALLET_INFO,COLLAPSIBLE_BILL_INFO,ITEMISATION_ENABLED,NO_BAG,NO_BAG_DELIVERY_V3';
|
|
20
|
+
|
|
21
|
+
function baseHeaders(session, extra = {}) {
|
|
22
|
+
return {
|
|
23
|
+
'content-type': 'application/json; charset=utf-8',
|
|
24
|
+
'x-requested-with': 'XMLHttpRequest',
|
|
25
|
+
'user-agent': 'okhttp/4.12.0',
|
|
26
|
+
appversion: '26.5.5', app_version: '26.5.5',
|
|
27
|
+
platform: 'android', tenant: 'ZEPTO',
|
|
28
|
+
sessionid: session.sessionId, session_id: session.sessionId,
|
|
29
|
+
deviceuid: session.deviceUid, device_uid: session.deviceUid,
|
|
30
|
+
device_brand: 'samsung', device_model: 'SM-G991B',
|
|
31
|
+
system_version: '13', systemversion: '13',
|
|
32
|
+
source: 'PLAY_STORE', is_internal_user: 'false', isinternaluser: 'false',
|
|
33
|
+
...(session.token ? { authorization: `Bearer ${session.token}` } : {}),
|
|
34
|
+
...extra,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cartHeaders(session, storeId, extra = {}) {
|
|
39
|
+
return baseHeaders(session, {
|
|
40
|
+
compatible_components: COMPATIBLE_COMPONENTS,
|
|
41
|
+
marketplace_type: 'SUPER_SAVER', experience_variant: 'SS_UNIFIED',
|
|
42
|
+
performance_mode: 'NORMAL', auth_revamp_flow: 'v2',
|
|
43
|
+
is_new_font: 'true', accessibility_layout: 'false',
|
|
44
|
+
...(storeId ? { store_id: storeId, storeid: storeId, store_ids: storeId } : {}),
|
|
45
|
+
...extra,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function req(host, method, path, hdrs, bodyObj) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
const body = bodyObj != null ? JSON.stringify(bodyObj) : null;
|
|
52
|
+
const h = { ...hdrs, host };
|
|
53
|
+
if (body) h['content-length'] = Buffer.byteLength(body);
|
|
54
|
+
const r = https.request({ hostname: host, port: 443, method, path, headers: h, timeout: 20000 }, (res) => {
|
|
55
|
+
const ch = [];
|
|
56
|
+
res.on('data', (c) => ch.push(c));
|
|
57
|
+
res.on('end', () => {
|
|
58
|
+
const text = Buffer.concat(ch).toString('utf8');
|
|
59
|
+
let json = null;
|
|
60
|
+
try { json = JSON.parse(text); } catch { /* */ }
|
|
61
|
+
resolve({ status: res.statusCode, text, json });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
r.on('error', (e) => resolve({ status: 0, text: 'err:' + e.message, json: null }));
|
|
65
|
+
r.on('timeout', () => { r.destroy(); resolve({ status: 0, text: 'timeout', json: null }); });
|
|
66
|
+
if (body) r.write(body);
|
|
67
|
+
r.end();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build a coupon-check session from a logged-in zepto provider session.
|
|
72
|
+
function couponSession(session) {
|
|
73
|
+
return {
|
|
74
|
+
sessionId: session.sessionId || uuid(),
|
|
75
|
+
deviceUid: session.deviceUid || session.deviceId || randHex(16),
|
|
76
|
+
token: session.token || session.accessToken,
|
|
77
|
+
user: session.user || (session.userId ? { id: session.userId } : null),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Parse coupon banners/codes out of a response. [{code, amount}].
|
|
82
|
+
function parseCoupons(text) {
|
|
83
|
+
const found = new Map();
|
|
84
|
+
if (!text) return [];
|
|
85
|
+
for (const m of text.matchAll(/Z-CARTBOOST(\d+)/g)) {
|
|
86
|
+
found.set(`Z-CARTBOOST${m[1]}`, { code: `Z-CARTBOOST${m[1]}`, amount: Number(m[1]) });
|
|
87
|
+
}
|
|
88
|
+
for (const m of text.matchAll(/(?:Extra|FLAT|Get|Unlock(?:ed)?)\s*₹\s*(\d{2,3})\s*OFF/gi)) {
|
|
89
|
+
const amt = Number(m[1]);
|
|
90
|
+
if (!found.has(`Z-CARTBOOST${amt}`)) found.set(`banner${amt}`, { code: null, amount: amt, banner: true });
|
|
91
|
+
}
|
|
92
|
+
return [...found.values()];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function resolveStore(session, lat, lng) {
|
|
96
|
+
const r = await req(GW, 'GET',
|
|
97
|
+
`/serviceability-service/api/v1/serviceability?lat=${lat}&long=${lng}&includeAddressDetails=true`,
|
|
98
|
+
cartHeaders(session));
|
|
99
|
+
let primary = null, anyServ = null;
|
|
100
|
+
const re = /\{"serviceable":(true|false),"storeId":"([0-9a-f-]{36})","storeConstruct":"([^"]+)"[^}]*?"isNightlyStore":(true|false)/g;
|
|
101
|
+
let m;
|
|
102
|
+
while ((m = re.exec(r.text))) {
|
|
103
|
+
const s = { serviceable: m[1] === 'true', storeId: m[2], construct: m[3] };
|
|
104
|
+
if (s.serviceable && !anyServ) anyServ = s;
|
|
105
|
+
if (s.serviceable && s.construct === 'PRIMARY_STORE' && !primary) primary = s;
|
|
106
|
+
}
|
|
107
|
+
return primary || anyServ;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function findProduct(session, storeId, lat, lng, query) {
|
|
111
|
+
const r = await req(SEARCH, 'POST', '/api/v3/search',
|
|
112
|
+
cartHeaders(session, storeId, {
|
|
113
|
+
storeid: storeId, store_id: storeId, store_ids: storeId,
|
|
114
|
+
store_etas: JSON.stringify({ [storeId]: -1 }),
|
|
115
|
+
store_serviceability: JSON.stringify({ data: { [storeId]: { type: 'PRIMARY_STORE' } } }),
|
|
116
|
+
gps_latitude: String(lat), gps_longitude: String(lng),
|
|
117
|
+
}),
|
|
118
|
+
{ query, userSessionId: '', mode: 'TYPED', intentId: uuid(), cartLastUpdatedAt: 0,
|
|
119
|
+
user_action_meta: [], requestMeta: {}, pageNumber: 0, retryCount: 0 });
|
|
120
|
+
const idx = r.text.indexOf('"storeProductId"');
|
|
121
|
+
if (idx < 0) return null;
|
|
122
|
+
const block = r.text.slice(Math.max(0, idx - 500), idx + 500);
|
|
123
|
+
const sp = block.match(/"storeProductId":"([0-9a-f-]{36})"/);
|
|
124
|
+
const pv = block.match(/"productVariantId":"([0-9a-f-]{36})"/);
|
|
125
|
+
const pr = block.match(/"(?:discountedSellingPrice|sellingPrice)":(\d+)/);
|
|
126
|
+
if (!sp || !pv) return null;
|
|
127
|
+
return { storeProductId: sp[1], productVariantId: pv[1], price: pr ? Number(pr[1]) : 10000 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Run the coupon check. Returns { coupons:[{code,amount}], status?, reason? }.
|
|
131
|
+
async function fetchCoupons(session, opts = {}) {
|
|
132
|
+
const lat = opts.lat ?? 12.9263597;
|
|
133
|
+
const lng = opts.lng ?? 77.5838125;
|
|
134
|
+
|
|
135
|
+
let STORE_ID = opts.storeId;
|
|
136
|
+
if (!STORE_ID) STORE_ID = (await resolveStore(session, lat, lng))?.storeId;
|
|
137
|
+
if (!STORE_ID) STORE_ID = '5ec071fd-78df-41f6-b3ae-7298d9f96a3d';
|
|
138
|
+
|
|
139
|
+
// build a cart >= MOV (first 2 in-stock common items)
|
|
140
|
+
const PRODUCTS = [];
|
|
141
|
+
for (const q of ['water', 'milk', 'bread', 'redbull', 'chips']) {
|
|
142
|
+
if (PRODUCTS.length >= 2) break;
|
|
143
|
+
const p = await findProduct(session, STORE_ID, lat, lng, q);
|
|
144
|
+
if (p && !PRODUCTS.find((x) => x.productVariantId === p.productVariantId)) PRODUCTS.push(p);
|
|
145
|
+
}
|
|
146
|
+
if (PRODUCTS.length === 0) {
|
|
147
|
+
PRODUCTS.push({ storeProductId: 'b382dc23-0d70-4a50-82f3-5219a0bb8b12', productVariantId: '3e01e4c1-b31d-4305-810f-5c3a03a7401f', price: 21900 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let cartId = '';
|
|
151
|
+
const cart = await req(GW, 'POST', '/cart-service/api/v1/cart/widget-action', cartHeaders(session, STORE_ID), {
|
|
152
|
+
actions: [{ widgetId: 'GET_CART_ID_WIDGET', actionType: 'GET_CART_ID', data: {} }],
|
|
153
|
+
});
|
|
154
|
+
const cm = cart.text.match(/"cartId"\s*:\s*"([0-9a-f-]{36})"/i) || cart.text.match(/"id"\s*:\s*"([0-9a-f-]{36})"/i);
|
|
155
|
+
if (cm) cartId = cm[1];
|
|
156
|
+
|
|
157
|
+
const cartProducts = PRODUCTS.map((p) => ({
|
|
158
|
+
quantity: 1, productVariantId: p.productVariantId, isAddedToGiftBag: false,
|
|
159
|
+
discountedSellingPrice: p.price, uclId: '', storeProductId: p.storeProductId,
|
|
160
|
+
}));
|
|
161
|
+
const putCart = await req(GW, 'PUT', '/cfs/api/v1/cart', cartHeaders(session, STORE_ID), {
|
|
162
|
+
storeId: STORE_ID, cartId, cartProducts,
|
|
163
|
+
cartProductsV2: cartProducts.map((p) => ({ cartProductId: p.productVariantId, ...p, addOns: [] })),
|
|
164
|
+
latitude: lat, longitude: lng,
|
|
165
|
+
deliveryInstructions: { leaveAtGate: false, additionalNote: '', doNotRingBell: false, bewareOfPets: false, returnPaperBag: false, leaveWithSecurity: false, returnCokePetBottle: false },
|
|
166
|
+
riderTip: 0, use_zepto_cash: true, zeptoPass: { isPassSelected: false },
|
|
167
|
+
isStoreChanged: false, isNightlyStore: false, manuallyAppliedFees: [], addAndSavePvIdsMap: {}, noBagDelivery: null,
|
|
168
|
+
userMeta: { whatsappOpted: 'PENDING', refresh: false, currentToPay: 0, currentGrandTotal: 0 },
|
|
169
|
+
userPreferences: { MANUAL_BXGY_BENEFIT: { opted: false }, Z_COINS: { opted: false } },
|
|
170
|
+
sessionMeta: { hasMovCrossed: true, lastMarketplaceType: 'SUPER_SAVER', toastNudgeViewed: {} },
|
|
171
|
+
openElements: { isPtpBottomSheetOpen: false, ptpBottomSheetLayoutId: '' },
|
|
172
|
+
user_action_meta: cartProducts.map((p) => ['atc', p.productVariantId]),
|
|
173
|
+
widgetLevelActions: {}, locationDistanceFromAddress: -1, installedUpiApps: [], isCredPayEligible: false,
|
|
174
|
+
isPtpLayoutMigrationEnabled: true, seenElementsInSession: { ptpBottomSheet: false, superSaverCart: true, seenWidgets: {} },
|
|
175
|
+
});
|
|
176
|
+
const pm = putCart.text.match(/"cartId"\s*:\s*"([0-9a-f-]{36})"/i);
|
|
177
|
+
if (pm) cartId = pm[1];
|
|
178
|
+
|
|
179
|
+
// store closed / paused → can't compute coupon
|
|
180
|
+
const ndr = putCart.text.match(/"nonDeliverableReason"\s*:\s*"([^"]+)"/);
|
|
181
|
+
const deliverable = /"isDeliverable"\s*:\s*true/.test(putCart.text);
|
|
182
|
+
if (!deliverable && ndr) {
|
|
183
|
+
return { coupons: [], status: 'store_unavailable', reason: ndr[1] };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// authoritative coupon source
|
|
187
|
+
const fl = await req(GW, 'POST', '/cfs/api/v1/cart/coupons/fetch-list', cartHeaders(session, STORE_ID), {
|
|
188
|
+
page_type: 'COUPON_REVAMP', useZCoins: false, activeTab: 'COUPONS_TAB', enableTabbedView: true,
|
|
189
|
+
props: {}, renderHeader: true, storeId: STORE_ID, cartId, latitude: lat, longitude: lng,
|
|
190
|
+
userAddressId: '', useZeptoCash: false, removedCampaignProducts: [], installedUpiApps: [], isCredPayEligible: false,
|
|
191
|
+
});
|
|
192
|
+
let coupons = parseCoupons(fl.text);
|
|
193
|
+
|
|
194
|
+
if (coupons.length === 0) {
|
|
195
|
+
const bwd = await req(GW, 'POST', '/cfs/api/v1/bulk-widget-data', cartHeaders(session, STORE_ID), {
|
|
196
|
+
widgets: [{ id: 100007034, pageIdentifier: 'cart_page', subscriptions: ['cart_change'] }],
|
|
197
|
+
cartProducts: PRODUCTS.map((p) => ({ storeProductId: p.storeProductId, productVariantId: p.productVariantId, mrp: p.price, markedPrice: p.price, quantity: 1, isDiscountApplicable: true, meta: {}, uclId: '' })),
|
|
198
|
+
enrichCall: true, campaignIds: [], couponIds: [], alreadyShownNudgeThresholds: [], alreadyShownNudgeThresholdsV2: [],
|
|
199
|
+
availedCampaignProducts: [], removedCampaigns: [], showAllOffers: true,
|
|
200
|
+
cartId, upsellPolicy: '', zeptoPassInfo: { isAddedToCart: false }, userActionMeta: [], latitude: lat, longitude: lng,
|
|
201
|
+
});
|
|
202
|
+
coupons = parseCoupons(bwd.text);
|
|
203
|
+
}
|
|
204
|
+
return { coupons, status: 'ok' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Classify the result into a tier. Returns { tier: 'rs50'|'rs75'|'rs100'|'normal'|'unchecked', amount, label }.
|
|
208
|
+
export function classifyCoupon(result) {
|
|
209
|
+
if (!result || result.status === 'store_unavailable') {
|
|
210
|
+
return { tier: 'unchecked', amount: null, label: 'store closed — unchecked' };
|
|
211
|
+
}
|
|
212
|
+
const coupons = result.coupons || [];
|
|
213
|
+
const coded = coupons.filter((c) => c.code && /Z-CARTBOOST/.test(c.code));
|
|
214
|
+
if (coded.length) {
|
|
215
|
+
const amt = Math.max(...coded.map((c) => c.amount || 0));
|
|
216
|
+
return tierFor(amt, 'code');
|
|
217
|
+
}
|
|
218
|
+
const tierAmts = [...new Set(coupons.map((c) => c.amount).filter((a) => [50, 75, 100].includes(a)))];
|
|
219
|
+
const allAmts = [...new Set(coupons.map((c) => c.amount).filter(Boolean))];
|
|
220
|
+
if (allAmts.length >= 3) return { tier: 'normal', amount: 0, label: 'normal (no coupon)' };
|
|
221
|
+
if (tierAmts.length === 1) return tierFor(tierAmts[0], 'banner');
|
|
222
|
+
if (tierAmts.length > 1) return tierFor(Math.max(...tierAmts), 'banner');
|
|
223
|
+
return { tier: 'normal', amount: 0, label: 'normal (no coupon)' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function tierFor(amt) {
|
|
227
|
+
if (amt >= 100) return { tier: 'rs100', amount: 100, label: '₹100 off' };
|
|
228
|
+
if (amt >= 75) return { tier: 'rs75', amount: 75, label: '₹75 off' };
|
|
229
|
+
if (amt >= 50) return { tier: 'rs50', amount: 50, label: '₹50 off' };
|
|
230
|
+
return { tier: 'normal', amount: 0, label: 'normal (no coupon)' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Top-level: given a logged-in zepto session, return the classified coupon tier.
|
|
234
|
+
export async function checkCouponTier(session, opts = {}) {
|
|
235
|
+
const cs = couponSession(session);
|
|
236
|
+
if (!cs.token) return { tier: 'unchecked', amount: null, label: 'no token' };
|
|
237
|
+
let result;
|
|
238
|
+
try { result = await fetchCoupons(cs, opts); }
|
|
239
|
+
catch (e) { return { tier: 'unchecked', amount: null, label: `check failed: ${e.message}` }; }
|
|
240
|
+
return { ...classifyCoupon(result), coupons: result.coupons };
|
|
241
|
+
}
|
package/src/worker.js
CHANGED
|
@@ -555,11 +555,12 @@ async function releaseNumber(provider, number, rentedAt, { log = () => {}, warn
|
|
|
555
555
|
export const __releaseNumberForTest = releaseNumber;
|
|
556
556
|
|
|
557
557
|
export class ZeptoWorker {
|
|
558
|
-
constructor(provider, { requested, concurrency, certSha, onEvent }) {
|
|
558
|
+
constructor(provider, { requested, concurrency, certSha, onEvent, checkCoupon }) {
|
|
559
559
|
this.provider = provider;
|
|
560
560
|
this.requested = requested;
|
|
561
561
|
this.concurrency = Math.max(1, Math.min(concurrency, requested));
|
|
562
562
|
this.certSha = certSha;
|
|
563
|
+
this.checkCoupon = !!checkCoupon; // also classify the ₹50/₹75/₹100 coupon tier
|
|
563
564
|
this.onEvent = onEvent || (() => {});
|
|
564
565
|
// Surface OTPCart re-logins (session taken over by another login) in the UI.
|
|
565
566
|
if (provider && 'onNotice' in provider) provider.onNotice = (msg) => this.onEvent('warn', `⚠ ${msg}`);
|
|
@@ -727,9 +728,20 @@ export class ZeptoWorker {
|
|
|
727
728
|
if (!v.ok) return fail(`verify: ${v.error}`);
|
|
728
729
|
|
|
729
730
|
const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), this.certSha);
|
|
730
|
-
this._emit(slot, { phase: 'saving', detail: 'encoding envelope' });
|
|
731
731
|
res.status = 'success'; res.cost = number.cost; res.session = zepto.toSessionKeys(session); res.envelope = envelope;
|
|
732
|
-
|
|
732
|
+
|
|
733
|
+
// Optional coupon-tier check — token is fresh right now (just verified).
|
|
734
|
+
if (this.checkCoupon) {
|
|
735
|
+
this._emit(slot, { phase: 'coupon', detail: 'checking coupon tier' });
|
|
736
|
+
try {
|
|
737
|
+
const { checkCouponTier } = await import('./providers/zeptoCoupon.js');
|
|
738
|
+
const c = await checkCouponTier(session);
|
|
739
|
+
res.coupon_tier = c.tier; res.coupon_label = c.label; res.coupon_amount = c.amount;
|
|
740
|
+
} catch { res.coupon_tier = 'unchecked'; }
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
this._emit(slot, { phase: 'saving', detail: 'encoding envelope' });
|
|
744
|
+
releaseOnce(true, res.coupon_label || 'extracted', true); // success: OTP consumed → no cancel
|
|
733
745
|
return res;
|
|
734
746
|
} catch (e) {
|
|
735
747
|
return fail(`unexpected: ${e.message}`);
|