scripter-x 1.0.28 → 1.0.30
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 +84 -17
- 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,9 +8,10 @@ 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
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
14
|
+
import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from 'node:fs';
|
|
14
15
|
import { Worker } from './worker.js';
|
|
15
16
|
import { RunController } from './controller.js';
|
|
16
17
|
import { STATUS } from './theme.js';
|
|
@@ -288,12 +289,47 @@ 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 base dir for Zepto envelope output.
|
|
293
|
+
function zeptoBaseDir(args) {
|
|
294
|
+
if (args.out) {
|
|
295
|
+
const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
|
|
296
|
+
return /\.(txt|json)$/i.test(expanded) ? dirname(expanded) : expanded;
|
|
297
|
+
}
|
|
298
|
+
const downloads = join(homedir(), 'Downloads');
|
|
299
|
+
return join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Save one envelope. With a tier → APPEND to {tier}.txt (one envelope per block,
|
|
303
|
+
// blank-line separated, so the file is readable + paste-friendly). Without a tier →
|
|
304
|
+
// a single per-number timestamped file (legacy behaviour). Returns the path written.
|
|
305
|
+
function saveZeptoEnvelope(args, number, envelope, tier) {
|
|
306
|
+
const baseDir = zeptoBaseDir(args);
|
|
307
|
+
if (tier) {
|
|
308
|
+
mkdirSync(baseDir, { recursive: true });
|
|
309
|
+
const dest = join(baseDir, `${tier}.txt`);
|
|
310
|
+
// append with a blank-line separator if the file already has content
|
|
311
|
+
const block = (existsSync(dest) ? '\n' : '') + envelope + '\n';
|
|
312
|
+
appendFileSync(dest, block);
|
|
313
|
+
return dest;
|
|
314
|
+
}
|
|
315
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
316
|
+
const dest = join(baseDir, `${number}-${stamp}.txt`);
|
|
317
|
+
mkdirSync(baseDir, { recursive: true });
|
|
318
|
+
writeFileSync(dest, envelope + '\n');
|
|
319
|
+
return dest;
|
|
320
|
+
}
|
|
321
|
+
|
|
291
322
|
export async function zeptoCmd(io, api, args = {}) {
|
|
292
323
|
const mode = args.manual ? 'manual' : (args.auto ? 'auto' : (await io.select('Choose Zepto login flow', [
|
|
293
324
|
{ label: 'Auto', value: 'auto', description: 'Headless extraction via OTPCart (Recommended)' },
|
|
294
325
|
{ label: 'Manual', value: 'manual', description: 'Enter phone + type OTP manually' }
|
|
295
326
|
])).value || 'auto');
|
|
296
327
|
|
|
328
|
+
// Optional: check the coupon tier (₹50/₹75/₹100/normal) per account + split into
|
|
329
|
+
// 4 files. Runs headlessly right after verify while the token is fresh.
|
|
330
|
+
const checkCoupon = args.checkCoupon != null ? args.checkCoupon
|
|
331
|
+
: await io.confirm('check coupon tier (₹50/₹75/₹100) + split into files?', false);
|
|
332
|
+
|
|
297
333
|
if (mode === 'manual') {
|
|
298
334
|
io.print(' ◉ Zepto — local OTP → ZAUTH1 envelope (Manual)');
|
|
299
335
|
io.print(' ◉ press esc to cancel', 'accent');
|
|
@@ -335,21 +371,18 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
335
371
|
// Build the ZAUTH1 envelope — the format the ZeptoAuthManager tool imports.
|
|
336
372
|
const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), args.cert);
|
|
337
373
|
|
|
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);
|
|
374
|
+
// Optional coupon-tier check (token is fresh right now) → split into 4 files.
|
|
375
|
+
let tier = '';
|
|
376
|
+
if (checkCoupon) {
|
|
377
|
+
io.print(' ◉ checking coupon tier …');
|
|
378
|
+
const c = await checkCouponTier(session);
|
|
379
|
+
tier = c.tier; // rs50 | rs75 | rs100 | normal | unchecked
|
|
380
|
+
io.print(` ✓ coupon: ${c.label}`, c.tier === 'normal' || c.tier === 'unchecked' ? undefined : 'accent');
|
|
348
381
|
}
|
|
349
|
-
|
|
350
|
-
|
|
382
|
+
|
|
383
|
+
const dest = saveZeptoEnvelope(args, number, envelope, tier);
|
|
351
384
|
done++;
|
|
352
|
-
io.print(
|
|
385
|
+
io.print(` ✓ envelope saved${tier ? ` (${tier})` : ''} to:`);
|
|
353
386
|
io.print(` ${dest}`, 'accent');
|
|
354
387
|
io.print(' ◉ paste into ZeptoAuthManager → Import:');
|
|
355
388
|
io.print(` ${envelope}`, 'accent');
|
|
@@ -412,6 +445,7 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
412
445
|
requested: count,
|
|
413
446
|
concurrency,
|
|
414
447
|
certSha: cert,
|
|
448
|
+
checkCoupon,
|
|
415
449
|
onEvent: controller.handleEvent,
|
|
416
450
|
});
|
|
417
451
|
controller.stop = () => worker.stop();
|
|
@@ -450,11 +484,29 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
450
484
|
io.print(` ✓ saved ${saved.count} session(s) to 2 files:`);
|
|
451
485
|
io.print(` full → ${saved.fullPath}`, 'accent');
|
|
452
486
|
io.print(` zauth → ${saved.zauthPath}`, 'accent');
|
|
487
|
+
}
|
|
488
|
+
// If coupon-tier was checked, ALSO split the envelopes into per-tier files.
|
|
489
|
+
if (checkCoupon) {
|
|
490
|
+
const tiers = {};
|
|
491
|
+
for (const r of results) {
|
|
492
|
+
if (r.status !== 'success') continue;
|
|
493
|
+
const tier = r.coupon_tier || 'unchecked';
|
|
494
|
+
(tiers[tier] = tiers[tier] || []).push(r.envelope);
|
|
495
|
+
}
|
|
496
|
+
io.print(' ◉ coupon tiers:');
|
|
497
|
+
for (const tier of ['rs100', 'rs75', 'rs50', 'normal', 'unchecked']) {
|
|
498
|
+
if (!tiers[tier]?.length) continue;
|
|
499
|
+
const dir = zeptoTierDir(args, name);
|
|
500
|
+
const dest = join(dir, `${tier}.txt`);
|
|
501
|
+
mkdirSync(dir, { recursive: true });
|
|
502
|
+
// one envelope per block, separated by a blank line — readable + paste-friendly
|
|
503
|
+
writeFileSync(dest, tiers[tier].join('\n\n') + '\n');
|
|
504
|
+
io.print(` ${TIER_LABEL[tier]} (${tiers[tier].length}) → ${dest}`, 'accent');
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
453
507
|
io.print(' ◉ ZAUTH1 envelopes:');
|
|
454
508
|
for (const r of results) {
|
|
455
|
-
if (r.status === 'success') {
|
|
456
|
-
io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
|
|
457
|
-
}
|
|
509
|
+
if (r.status === 'success') io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
|
|
458
510
|
}
|
|
459
511
|
}
|
|
460
512
|
} else {
|
|
@@ -462,6 +514,21 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
462
514
|
}
|
|
463
515
|
}
|
|
464
516
|
|
|
517
|
+
const TIER_LABEL = { rs100: '🟢 ₹100', rs75: '🟢 ₹75', rs50: '🟢 ₹50', normal: '⚪ normal', unchecked: '❔ unchecked' };
|
|
518
|
+
|
|
519
|
+
// The directory the per-tier files go in for an auto run.
|
|
520
|
+
function zeptoTierDir(args, name) {
|
|
521
|
+
let baseDir;
|
|
522
|
+
if (args.out) {
|
|
523
|
+
const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
|
|
524
|
+
baseDir = /\.(txt|json)$/i.test(expanded) ? dirname(expanded) : expanded;
|
|
525
|
+
} else {
|
|
526
|
+
const downloads = join(homedir(), 'Downloads');
|
|
527
|
+
baseDir = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto');
|
|
528
|
+
}
|
|
529
|
+
return join(baseDir, name);
|
|
530
|
+
}
|
|
531
|
+
|
|
465
532
|
async function saveZeptoSessions(results, name, out) {
|
|
466
533
|
const { statSync } = await import('node:fs');
|
|
467
534
|
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}`);
|