vantuz 3.4.2 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +45 -45
- package/admin-keygen.js +51 -0
- package/cli.js +685 -585
- package/config.js +733 -733
- package/core/agent-loop.js +190 -190
- package/core/ai-provider.js +298 -261
- package/core/automation.js +523 -523
- package/core/brand-analyst.js +101 -0
- package/core/channels.js +167 -167
- package/core/dashboard.js +230 -230
- package/core/database.js +135 -37
- package/core/eia-monitor.js +3 -1
- package/core/engine.js +648 -636
- package/core/gateway.js +447 -447
- package/core/learning.js +214 -214
- package/core/license.js +113 -0
- package/core/marketplace-adapter.js +168 -168
- package/core/memory.js +190 -190
- package/core/migrations/001-initial-schema.sql +1 -1
- package/core/queue.js +120 -120
- package/core/self-healer.js +314 -314
- package/core/unified-product.js +214 -214
- package/core/vision-service.js +113 -113
- package/index.js +217 -174
- package/modules/crm/sentiment-crm.js +231 -231
- package/modules/healer/listing-healer.js +201 -201
- package/modules/oracle/predictor.js +214 -214
- package/modules/researcher/agent.js +169 -169
- package/modules/team/agents/base.js +92 -92
- package/modules/team/agents/dev.js +33 -33
- package/modules/team/agents/josh.js +40 -40
- package/modules/team/agents/marketing.js +33 -33
- package/modules/team/agents/milo.js +36 -36
- package/modules/team/index.js +78 -78
- package/modules/team/shared-memory.js +87 -87
- package/modules/war-room/competitor-tracker.js +250 -250
- package/modules/war-room/pricing-engine.js +308 -308
- package/nodes/warehouse.js +238 -238
- package/onboard.js +1 -1
- package/package.json +7 -5
- package/platforms/pttavm.js +14 -14
- package/plugins/vantuz/index.js +528 -528
- package/plugins/vantuz/memory/hippocampus.js +465 -465
- package/plugins/vantuz/package.json +20 -20
- package/plugins/vantuz/platforms/_template.js +118 -118
- package/plugins/vantuz/platforms/amazon.js +236 -236
- package/plugins/vantuz/platforms/ciceksepeti.js +166 -166
- package/plugins/vantuz/platforms/hepsiburada.js +180 -180
- package/plugins/vantuz/platforms/index.js +165 -165
- package/plugins/vantuz/platforms/n11.js +229 -229
- package/plugins/vantuz/platforms/pazarama.js +154 -154
- package/plugins/vantuz/platforms/pttavm.js +127 -127
- package/plugins/vantuz/platforms/trendyol.js +326 -326
- package/plugins/vantuz/services/alerts.js +253 -253
- package/plugins/vantuz/services/license.js +34 -34
- package/plugins/vantuz/services/scheduler.js +232 -232
- package/plugins/vantuz/tools/analytics.js +152 -152
- package/plugins/vantuz/tools/crossborder.js +187 -187
- package/plugins/vantuz/tools/nl-parser.js +211 -211
- package/plugins/vantuz/tools/product.js +110 -110
- package/plugins/vantuz/tools/quick-report.js +175 -175
- package/plugins/vantuz/tools/repricer.js +314 -314
- package/plugins/vantuz/tools/sentiment.js +115 -115
- package/plugins/vantuz/tools/vision.js +257 -257
- package/private.pem +28 -0
- package/public.pem +9 -0
- package/server/app.js +260 -260
- package/server/public/index.html +514 -514
- package/start.bat +33 -33
- package/vantuz.sqlite +0 -0
|
@@ -1,250 +1,250 @@
|
|
|
1
|
-
// modules/war-room/competitor-tracker.js
|
|
2
|
-
// Competitor Price Tracker for Vantuz OS V2
|
|
3
|
-
// Monitors rival prices with POLITENESS MODE to avoid IP bans.
|
|
4
|
-
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import os from 'os';
|
|
8
|
-
import { log } from '../../core/ai-provider.js';
|
|
9
|
-
|
|
10
|
-
const HISTORY_FILE = path.join(os.homedir(), '.vantuz', 'memory', 'competitor-history.json');
|
|
11
|
-
const MAX_HISTORY = 1000;
|
|
12
|
-
|
|
13
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
-
// POLITENESS MODE — Anti-Ban Protection
|
|
15
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
|
|
17
|
-
function randomDelay(minMs = 2000, maxMs = 5000) {
|
|
18
|
-
const delay = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
|
|
19
|
-
return new Promise(resolve => setTimeout(resolve, delay));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const RATE_LIMITS = new Map(); // platform -> { count, resetAt }
|
|
23
|
-
const MAX_REQUESTS_PER_HOUR = 120;
|
|
24
|
-
|
|
25
|
-
function checkRateLimit(platform) {
|
|
26
|
-
const now = Date.now();
|
|
27
|
-
let limit = RATE_LIMITS.get(platform);
|
|
28
|
-
|
|
29
|
-
if (!limit || now > limit.resetAt) {
|
|
30
|
-
limit = { count: 0, resetAt: now + 3600000 }; // 1 hour window
|
|
31
|
-
RATE_LIMITS.set(platform, limit);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (limit.count >= MAX_REQUESTS_PER_HOUR) {
|
|
35
|
-
log('WARN', `Rate limit reached for ${platform}`, {
|
|
36
|
-
count: limit.count,
|
|
37
|
-
resetIn: Math.round((limit.resetAt - now) / 60000) + ' min'
|
|
38
|
-
});
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
limit.count++;
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
47
|
-
// HISTORY PERSISTENCE
|
|
48
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
49
|
-
|
|
50
|
-
function loadHistory() {
|
|
51
|
-
try {
|
|
52
|
-
if (fs.existsSync(HISTORY_FILE)) {
|
|
53
|
-
return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
|
|
54
|
-
}
|
|
55
|
-
} catch (e) { /* ignore */ }
|
|
56
|
-
return {};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function saveHistory(history) {
|
|
60
|
-
const dir = path.dirname(HISTORY_FILE);
|
|
61
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
62
|
-
const tmp = HISTORY_FILE + '.tmp';
|
|
63
|
-
fs.writeFileSync(tmp, JSON.stringify(history, null, 2), 'utf-8');
|
|
64
|
-
fs.renameSync(tmp, HISTORY_FILE);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
-
// COMPETITOR TRACKER
|
|
69
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
-
|
|
71
|
-
class CompetitorTracker {
|
|
72
|
-
constructor() {
|
|
73
|
-
this.history = loadHistory(); // barcode -> [{timestamp, platform, competitors[]}]
|
|
74
|
-
this.adapters = null; // Set by init()
|
|
75
|
-
log('INFO', 'CompetitorTracker initialized', {
|
|
76
|
-
trackedProducts: Object.keys(this.history).length
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Set adapter registry reference.
|
|
82
|
-
*/
|
|
83
|
-
setAdapters(adapterRegistry) {
|
|
84
|
-
this.adapters = adapterRegistry;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Track competitor prices for a single product.
|
|
89
|
-
* @param {string} barcode
|
|
90
|
-
* @param {string} platform - Platform to check (default: all active).
|
|
91
|
-
* @returns {{ competitors: array, alert: string|null }}
|
|
92
|
-
*/
|
|
93
|
-
async trackProduct(barcode, platform = null) {
|
|
94
|
-
const competitors = [];
|
|
95
|
-
const platforms = platform
|
|
96
|
-
? [platform]
|
|
97
|
-
: (this.adapters?.getActive() || []).map(a => a.name);
|
|
98
|
-
|
|
99
|
-
for (const pName of platforms) {
|
|
100
|
-
// Rate limit check
|
|
101
|
-
if (!checkRateLimit(pName)) {
|
|
102
|
-
log('WARN', `Skipping ${pName} for ${barcode}: rate limited`);
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const adapter = this.adapters?.get(pName);
|
|
107
|
-
if (!adapter || typeof adapter.getCompetitorPrices !== 'function') {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
// POLITENESS MODE: Random delay before each request
|
|
113
|
-
await randomDelay(2000, 5000);
|
|
114
|
-
|
|
115
|
-
const result = await adapter.getCompetitorPrices(barcode);
|
|
116
|
-
if (result?.success && result.data) {
|
|
117
|
-
for (const comp of result.data) {
|
|
118
|
-
competitors.push({
|
|
119
|
-
platform: pName,
|
|
120
|
-
seller: comp.seller || comp.merchantName || 'unknown',
|
|
121
|
-
price: parseFloat(comp.price || comp.salePrice || 0),
|
|
122
|
-
stock: comp.stock ?? comp.hasStock ?? null,
|
|
123
|
-
rating: comp.rating || null,
|
|
124
|
-
fetchedAt: new Date().toISOString()
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch (e) {
|
|
129
|
-
log('ERROR', `Competitor fetch failed: ${pName}/${barcode}`, { error: e.message });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Store in history
|
|
134
|
-
this._recordHistory(barcode, competitors);
|
|
135
|
-
|
|
136
|
-
// Detect alerts
|
|
137
|
-
const alert = this._detectAlerts(barcode, competitors);
|
|
138
|
-
|
|
139
|
-
return { barcode, competitors, alert };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Track multiple products in batch (respects rate limits).
|
|
144
|
-
* @param {string[]} barcodes
|
|
145
|
-
*/
|
|
146
|
-
async trackBatch(barcodes) {
|
|
147
|
-
const results = [];
|
|
148
|
-
|
|
149
|
-
for (const barcode of barcodes) {
|
|
150
|
-
const result = await this.trackProduct(barcode);
|
|
151
|
-
results.push(result);
|
|
152
|
-
|
|
153
|
-
// Extra delay between products for politeness
|
|
154
|
-
await randomDelay(1000, 3000);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
log('INFO', `Batch tracking complete`, {
|
|
158
|
-
products: barcodes.length,
|
|
159
|
-
totalCompetitors: results.reduce((s, r) => s + r.competitors.length, 0)
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
return results;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get price history for a product.
|
|
167
|
-
*/
|
|
168
|
-
getHistory(barcode) {
|
|
169
|
-
return this.history[barcode] || [];
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get competitor summary: cheapest, average, count.
|
|
174
|
-
*/
|
|
175
|
-
getSummary(barcode) {
|
|
176
|
-
const entries = this.history[barcode] || [];
|
|
177
|
-
if (entries.length === 0) return null;
|
|
178
|
-
|
|
179
|
-
const latest = entries[entries.length - 1];
|
|
180
|
-
const prices = latest.competitors.map(c => c.price).filter(p => p > 0);
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
barcode,
|
|
184
|
-
sellerCount: latest.competitors.length,
|
|
185
|
-
cheapest: prices.length > 0 ? Math.min(...prices) : null,
|
|
186
|
-
average: prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : null,
|
|
187
|
-
mostExpensive: prices.length > 0 ? Math.max(...prices) : null,
|
|
188
|
-
lastChecked: latest.timestamp
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
193
|
-
// PRIVATE
|
|
194
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
195
|
-
|
|
196
|
-
_recordHistory(barcode, competitors) {
|
|
197
|
-
if (!this.history[barcode]) {
|
|
198
|
-
this.history[barcode] = [];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
this.history[barcode].push({
|
|
202
|
-
timestamp: new Date().toISOString(),
|
|
203
|
-
competitors
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Keep only recent entries per product
|
|
207
|
-
if (this.history[barcode].length > 50) {
|
|
208
|
-
this.history[barcode] = this.history[barcode].slice(-50);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Limit total history size
|
|
212
|
-
const total = Object.values(this.history).reduce((s, arr) => s + arr.length, 0);
|
|
213
|
-
if (total > MAX_HISTORY) {
|
|
214
|
-
// Remove oldest product histories
|
|
215
|
-
const sorted = Object.entries(this.history)
|
|
216
|
-
.sort((a, b) => a[1].length - b[1].length);
|
|
217
|
-
delete this.history[sorted[0][0]];
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
saveHistory(this.history);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
_detectAlerts(barcode, competitors) {
|
|
224
|
-
if (competitors.length === 0) return null;
|
|
225
|
-
|
|
226
|
-
const prices = competitors.map(c => c.price).filter(p => p > 0);
|
|
227
|
-
if (prices.length === 0) return null;
|
|
228
|
-
|
|
229
|
-
const cheapest = Math.min(...prices);
|
|
230
|
-
const lowStockSellers = competitors.filter(c => c.stock !== null && c.stock < 5);
|
|
231
|
-
|
|
232
|
-
// Alert: all competitors low stock → opportunity!
|
|
233
|
-
if (lowStockSellers.length > 0 && lowStockSellers.length >= competitors.length * 0.7) {
|
|
234
|
-
return `🔥 FIRSAT: ${barcode} — Rakiplerin %${Math.round(lowStockSellers.length / competitors.length * 100)}'i düşük stokta. Fiyat artırma fırsatı!`;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
let trackerInstance = null;
|
|
242
|
-
|
|
243
|
-
export function getCompetitorTracker() {
|
|
244
|
-
if (!trackerInstance) {
|
|
245
|
-
trackerInstance = new CompetitorTracker();
|
|
246
|
-
}
|
|
247
|
-
return trackerInstance;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export default CompetitorTracker;
|
|
1
|
+
// modules/war-room/competitor-tracker.js
|
|
2
|
+
// Competitor Price Tracker for Vantuz OS V2
|
|
3
|
+
// Monitors rival prices with POLITENESS MODE to avoid IP bans.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { log } from '../../core/ai-provider.js';
|
|
9
|
+
|
|
10
|
+
const HISTORY_FILE = path.join(os.homedir(), '.vantuz', 'memory', 'competitor-history.json');
|
|
11
|
+
const MAX_HISTORY = 1000;
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// POLITENESS MODE — Anti-Ban Protection
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
function randomDelay(minMs = 2000, maxMs = 5000) {
|
|
18
|
+
const delay = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
|
|
19
|
+
return new Promise(resolve => setTimeout(resolve, delay));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const RATE_LIMITS = new Map(); // platform -> { count, resetAt }
|
|
23
|
+
const MAX_REQUESTS_PER_HOUR = 120;
|
|
24
|
+
|
|
25
|
+
function checkRateLimit(platform) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
let limit = RATE_LIMITS.get(platform);
|
|
28
|
+
|
|
29
|
+
if (!limit || now > limit.resetAt) {
|
|
30
|
+
limit = { count: 0, resetAt: now + 3600000 }; // 1 hour window
|
|
31
|
+
RATE_LIMITS.set(platform, limit);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (limit.count >= MAX_REQUESTS_PER_HOUR) {
|
|
35
|
+
log('WARN', `Rate limit reached for ${platform}`, {
|
|
36
|
+
count: limit.count,
|
|
37
|
+
resetIn: Math.round((limit.resetAt - now) / 60000) + ' min'
|
|
38
|
+
});
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
limit.count++;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
47
|
+
// HISTORY PERSISTENCE
|
|
48
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
49
|
+
|
|
50
|
+
function loadHistory() {
|
|
51
|
+
try {
|
|
52
|
+
if (fs.existsSync(HISTORY_FILE)) {
|
|
53
|
+
return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
|
|
54
|
+
}
|
|
55
|
+
} catch (e) { /* ignore */ }
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveHistory(history) {
|
|
60
|
+
const dir = path.dirname(HISTORY_FILE);
|
|
61
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
const tmp = HISTORY_FILE + '.tmp';
|
|
63
|
+
fs.writeFileSync(tmp, JSON.stringify(history, null, 2), 'utf-8');
|
|
64
|
+
fs.renameSync(tmp, HISTORY_FILE);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// COMPETITOR TRACKER
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
+
|
|
71
|
+
class CompetitorTracker {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.history = loadHistory(); // barcode -> [{timestamp, platform, competitors[]}]
|
|
74
|
+
this.adapters = null; // Set by init()
|
|
75
|
+
log('INFO', 'CompetitorTracker initialized', {
|
|
76
|
+
trackedProducts: Object.keys(this.history).length
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set adapter registry reference.
|
|
82
|
+
*/
|
|
83
|
+
setAdapters(adapterRegistry) {
|
|
84
|
+
this.adapters = adapterRegistry;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Track competitor prices for a single product.
|
|
89
|
+
* @param {string} barcode
|
|
90
|
+
* @param {string} platform - Platform to check (default: all active).
|
|
91
|
+
* @returns {{ competitors: array, alert: string|null }}
|
|
92
|
+
*/
|
|
93
|
+
async trackProduct(barcode, platform = null) {
|
|
94
|
+
const competitors = [];
|
|
95
|
+
const platforms = platform
|
|
96
|
+
? [platform]
|
|
97
|
+
: (this.adapters?.getActive() || []).map(a => a.name);
|
|
98
|
+
|
|
99
|
+
for (const pName of platforms) {
|
|
100
|
+
// Rate limit check
|
|
101
|
+
if (!checkRateLimit(pName)) {
|
|
102
|
+
log('WARN', `Skipping ${pName} for ${barcode}: rate limited`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const adapter = this.adapters?.get(pName);
|
|
107
|
+
if (!adapter || typeof adapter.getCompetitorPrices !== 'function') {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// POLITENESS MODE: Random delay before each request
|
|
113
|
+
await randomDelay(2000, 5000);
|
|
114
|
+
|
|
115
|
+
const result = await adapter.getCompetitorPrices(barcode);
|
|
116
|
+
if (result?.success && result.data) {
|
|
117
|
+
for (const comp of result.data) {
|
|
118
|
+
competitors.push({
|
|
119
|
+
platform: pName,
|
|
120
|
+
seller: comp.seller || comp.merchantName || 'unknown',
|
|
121
|
+
price: parseFloat(comp.price || comp.salePrice || 0),
|
|
122
|
+
stock: comp.stock ?? comp.hasStock ?? null,
|
|
123
|
+
rating: comp.rating || null,
|
|
124
|
+
fetchedAt: new Date().toISOString()
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
log('ERROR', `Competitor fetch failed: ${pName}/${barcode}`, { error: e.message });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Store in history
|
|
134
|
+
this._recordHistory(barcode, competitors);
|
|
135
|
+
|
|
136
|
+
// Detect alerts
|
|
137
|
+
const alert = this._detectAlerts(barcode, competitors);
|
|
138
|
+
|
|
139
|
+
return { barcode, competitors, alert };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Track multiple products in batch (respects rate limits).
|
|
144
|
+
* @param {string[]} barcodes
|
|
145
|
+
*/
|
|
146
|
+
async trackBatch(barcodes) {
|
|
147
|
+
const results = [];
|
|
148
|
+
|
|
149
|
+
for (const barcode of barcodes) {
|
|
150
|
+
const result = await this.trackProduct(barcode);
|
|
151
|
+
results.push(result);
|
|
152
|
+
|
|
153
|
+
// Extra delay between products for politeness
|
|
154
|
+
await randomDelay(1000, 3000);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
log('INFO', `Batch tracking complete`, {
|
|
158
|
+
products: barcodes.length,
|
|
159
|
+
totalCompetitors: results.reduce((s, r) => s + r.competitors.length, 0)
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get price history for a product.
|
|
167
|
+
*/
|
|
168
|
+
getHistory(barcode) {
|
|
169
|
+
return this.history[barcode] || [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get competitor summary: cheapest, average, count.
|
|
174
|
+
*/
|
|
175
|
+
getSummary(barcode) {
|
|
176
|
+
const entries = this.history[barcode] || [];
|
|
177
|
+
if (entries.length === 0) return null;
|
|
178
|
+
|
|
179
|
+
const latest = entries[entries.length - 1];
|
|
180
|
+
const prices = latest.competitors.map(c => c.price).filter(p => p > 0);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
barcode,
|
|
184
|
+
sellerCount: latest.competitors.length,
|
|
185
|
+
cheapest: prices.length > 0 ? Math.min(...prices) : null,
|
|
186
|
+
average: prices.length > 0 ? prices.reduce((a, b) => a + b, 0) / prices.length : null,
|
|
187
|
+
mostExpensive: prices.length > 0 ? Math.max(...prices) : null,
|
|
188
|
+
lastChecked: latest.timestamp
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
193
|
+
// PRIVATE
|
|
194
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
_recordHistory(barcode, competitors) {
|
|
197
|
+
if (!this.history[barcode]) {
|
|
198
|
+
this.history[barcode] = [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.history[barcode].push({
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
competitors
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Keep only recent entries per product
|
|
207
|
+
if (this.history[barcode].length > 50) {
|
|
208
|
+
this.history[barcode] = this.history[barcode].slice(-50);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Limit total history size
|
|
212
|
+
const total = Object.values(this.history).reduce((s, arr) => s + arr.length, 0);
|
|
213
|
+
if (total > MAX_HISTORY) {
|
|
214
|
+
// Remove oldest product histories
|
|
215
|
+
const sorted = Object.entries(this.history)
|
|
216
|
+
.sort((a, b) => a[1].length - b[1].length);
|
|
217
|
+
delete this.history[sorted[0][0]];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
saveHistory(this.history);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_detectAlerts(barcode, competitors) {
|
|
224
|
+
if (competitors.length === 0) return null;
|
|
225
|
+
|
|
226
|
+
const prices = competitors.map(c => c.price).filter(p => p > 0);
|
|
227
|
+
if (prices.length === 0) return null;
|
|
228
|
+
|
|
229
|
+
const cheapest = Math.min(...prices);
|
|
230
|
+
const lowStockSellers = competitors.filter(c => c.stock !== null && c.stock < 5);
|
|
231
|
+
|
|
232
|
+
// Alert: all competitors low stock → opportunity!
|
|
233
|
+
if (lowStockSellers.length > 0 && lowStockSellers.length >= competitors.length * 0.7) {
|
|
234
|
+
return `🔥 FIRSAT: ${barcode} — Rakiplerin %${Math.round(lowStockSellers.length / competitors.length * 100)}'i düşük stokta. Fiyat artırma fırsatı!`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let trackerInstance = null;
|
|
242
|
+
|
|
243
|
+
export function getCompetitorTracker() {
|
|
244
|
+
if (!trackerInstance) {
|
|
245
|
+
trackerInstance = new CompetitorTracker();
|
|
246
|
+
}
|
|
247
|
+
return trackerInstance;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export default CompetitorTracker;
|