scripter-x 1.0.27 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripter-x",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "ScripterX — local Flipkart session extractor (runs on your residential IP, syncs to marthunt)",
5
5
  "type": "module",
6
6
  "bin": {
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
- // {phone}-{timestamp}.txt in the chosen dir (default ~/Downloads/scripterx/zepto)
339
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
340
- const fname = `${number}-${stamp}.txt`;
341
- let dest;
342
- if (args.out) {
343
- const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
344
- dest = /\.(txt|json)$/i.test(expanded) ? expanded : join(expanded, fname);
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
- mkdirSync(dirname(dest), { recursive: true });
350
- writeFileSync(dest, envelope + '\n');
379
+
380
+ const dest = saveZeptoEnvelope(args, number, envelope, tier);
351
381
  done++;
352
- io.print(' ✓ envelope saved to:');
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); }
@@ -3,9 +3,9 @@ import { extractOtp } from '../flipkart.js';
3
3
 
4
4
  const BASE = 'https://api.tempotp.online';
5
5
  // serviceId -> cost (₹). Keep ids as strings (they go straight into the GET params).
6
- export const SERVICES = { '1040': 10.0, '940': 16.0, '2451': 12.5, '2484': 18.0 };
6
+ export const SERVICES = { '1040': 10.0, '940': 16.0, '2451': 12.5, '2452': 12.5, '2453': 12.0, '2484': 18.0 };
7
7
  // optional friendly names shown in the service picker
8
- export const SERVICE_NAMES = { '1040': 'Flipkart', '940': 'Flipkart', '2451': 'Shopsy/Flipkart 1 (SERVER 16)', '2484': 'ALL 6 DIGIT (SERVER 17 [ALL])' };
8
+ export const SERVICE_NAMES = { '1040': 'Flipkart', '940': 'Flipkart', '2451': 'Shopsy/Flipkart 1 (SERVER 16)', '2452': 'Shopsy/Flipkart 2 (SERVER 16)', '2453': 'Shopsy/Flipkart 3 (SERVER 16)', '2484': 'ALL 6 DIGIT (SERVER 17 [ALL])' };
9
9
  export const DEFAULT_SERVICE = '940';
10
10
  const COUNTRY = '22';
11
11
 
@@ -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
- releaseOnce(true, 'extracted', true); // success: OTP consumed → no cancel
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}`);