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,308 +1,308 @@
|
|
|
1
|
-
// modules/war-room/pricing-engine.js
|
|
2
|
-
// Dynamic Pricing Engine for Vantuz OS V2
|
|
3
|
-
// Reads rules from BRAND.md, applies pricing strategies, includes KILL SWITCH.
|
|
4
|
-
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { log } from '../../core/ai-provider.js';
|
|
8
|
-
import { getCriticalQueue } from '../../core/queue.js';
|
|
9
|
-
|
|
10
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
-
// BRAND RULES PARSER
|
|
12
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
-
|
|
14
|
-
function parseBrandRules() {
|
|
15
|
-
const brandPath = path.join(process.cwd(), 'workspace', 'BRAND.md');
|
|
16
|
-
const defaults = {
|
|
17
|
-
minMargin: 15,
|
|
18
|
-
maxDiscount: 30,
|
|
19
|
-
killSwitchMargin: 5, // Kill switch activates below this
|
|
20
|
-
strategy: 'smart' // 'aggressive', 'smart', 'conservative'
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
if (!fs.existsSync(brandPath)) return defaults;
|
|
25
|
-
const content = fs.readFileSync(brandPath, 'utf-8');
|
|
26
|
-
|
|
27
|
-
// Parse min margin
|
|
28
|
-
const marginMatch = content.match(/Minimum Kar Marjı[:\s]*%?(\d+)/i);
|
|
29
|
-
if (marginMatch) defaults.minMargin = parseInt(marginMatch[1], 10);
|
|
30
|
-
|
|
31
|
-
// Parse max discount
|
|
32
|
-
const discountMatch = content.match(/Maksimum İndirim[:\s]*%?(\d+)/i);
|
|
33
|
-
if (discountMatch) defaults.maxDiscount = parseInt(discountMatch[1], 10);
|
|
34
|
-
|
|
35
|
-
// Parse strategy
|
|
36
|
-
if (content.includes('Agresif') || content.includes('aggressive')) {
|
|
37
|
-
defaults.strategy = 'aggressive';
|
|
38
|
-
} else if (content.includes('Muhafazakar') || content.includes('conservative')) {
|
|
39
|
-
defaults.strategy = 'conservative';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
log('INFO', 'Brand rules parsed', defaults);
|
|
43
|
-
} catch (e) {
|
|
44
|
-
log('WARN', 'Brand rules parse error, using defaults', { error: e.message });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return defaults;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
|
-
// KILL SWITCH
|
|
52
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
|
-
|
|
54
|
-
let killSwitchActive = false;
|
|
55
|
-
let killSwitchReason = '';
|
|
56
|
-
const killSwitchCallbacks = []; // webhook/notification callbacks
|
|
57
|
-
|
|
58
|
-
function activateKillSwitch(reason, productInfo) {
|
|
59
|
-
killSwitchActive = true;
|
|
60
|
-
killSwitchReason = reason;
|
|
61
|
-
|
|
62
|
-
log('ERROR', `🛑 KILL SWITCH ACTIVATED: ${reason}`, productInfo);
|
|
63
|
-
|
|
64
|
-
// Notify all registered callbacks
|
|
65
|
-
for (const cb of killSwitchCallbacks) {
|
|
66
|
-
try { cb({ reason, productInfo, timestamp: new Date().toISOString() }); }
|
|
67
|
-
catch (e) { /* swallow */ }
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function resetKillSwitch() {
|
|
72
|
-
killSwitchActive = false;
|
|
73
|
-
killSwitchReason = '';
|
|
74
|
-
log('INFO', '✅ Kill Switch reset — otonom mod devam ediyor');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
78
|
-
// PRICING ENGINE
|
|
79
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
|
-
|
|
81
|
-
class PricingEngine {
|
|
82
|
-
constructor() {
|
|
83
|
-
this.rules = parseBrandRules();
|
|
84
|
-
this.queue = getCriticalQueue();
|
|
85
|
-
this.decisions = []; // Recent decisions log
|
|
86
|
-
log('INFO', 'PricingEngine initialized', this.rules);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Register a callback for Kill Switch activation.
|
|
91
|
-
*/
|
|
92
|
-
onKillSwitch(callback) {
|
|
93
|
-
killSwitchCallbacks.push(callback);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Reload brand rules (e.g., after BRAND.md edit).
|
|
98
|
-
*/
|
|
99
|
-
reloadRules() {
|
|
100
|
-
this.rules = parseBrandRules();
|
|
101
|
-
log('INFO', 'Brand rules reloaded', this.rules);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Check kill switch status.
|
|
106
|
-
*/
|
|
107
|
-
isKillSwitchActive() {
|
|
108
|
-
return { active: killSwitchActive, reason: killSwitchReason };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Reset kill switch (manual override).
|
|
113
|
-
*/
|
|
114
|
-
resetKillSwitch() {
|
|
115
|
-
resetKillSwitch();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Calculate optimal price for a product.
|
|
120
|
-
* @param {object} product - Unified Product from catalog.
|
|
121
|
-
* @param {object[]} competitors - From CompetitorTracker.
|
|
122
|
-
* @returns {{ action, newPrice, reason, dryRun }}
|
|
123
|
-
*/
|
|
124
|
-
calculatePrice(product, competitors = []) {
|
|
125
|
-
const { price, cost, barcode } = product;
|
|
126
|
-
const { minMargin, killSwitchMargin, strategy } = this.rules;
|
|
127
|
-
|
|
128
|
-
// ── Kill Switch Check ──
|
|
129
|
-
if (cost > 0 && price > 0) {
|
|
130
|
-
const currentMargin = ((price - cost) / price) * 100;
|
|
131
|
-
if (currentMargin < killSwitchMargin) {
|
|
132
|
-
activateKillSwitch(
|
|
133
|
-
`Kar marjı %${currentMargin.toFixed(1)} — %${killSwitchMargin} sınırının altına düştü`,
|
|
134
|
-
{ barcode, price, cost, margin: currentMargin }
|
|
135
|
-
);
|
|
136
|
-
return {
|
|
137
|
-
action: 'KILL_SWITCH',
|
|
138
|
-
newPrice: price,
|
|
139
|
-
reason: `🛑 KILL SWITCH: Marj %${currentMargin.toFixed(1)}. Manuel onay gerekiyor.`,
|
|
140
|
-
dryRun: false
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── If kill switch is globally active ──
|
|
146
|
-
if (killSwitchActive) {
|
|
147
|
-
return {
|
|
148
|
-
action: 'BLOCKED',
|
|
149
|
-
newPrice: price,
|
|
150
|
-
reason: `🛑 Kill Switch aktif: ${killSwitchReason}. resetKillSwitch() ile açın.`,
|
|
151
|
-
dryRun: false
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── Price Floor ──
|
|
156
|
-
const minPrice = cost > 0 ? cost * (1 + minMargin / 100) : 0;
|
|
157
|
-
|
|
158
|
-
// ── No Competitors? ──
|
|
159
|
-
if (competitors.length === 0) {
|
|
160
|
-
if (strategy === 'aggressive') {
|
|
161
|
-
const newPrice = Math.round(price * 1.10); // +10%
|
|
162
|
-
return {
|
|
163
|
-
action: 'INCREASE',
|
|
164
|
-
newPrice,
|
|
165
|
-
reason: 'Tek satıcısın — %10 artış uygula (elastiklik testi)',
|
|
166
|
-
dryRun: false
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
action: 'HOLD',
|
|
171
|
-
newPrice: price,
|
|
172
|
-
reason: 'Tek satıcısın — fiyat korunuyor (muhafazakar mod)',
|
|
173
|
-
dryRun: false
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ── Competitor Analysis ──
|
|
178
|
-
const competitorPrices = competitors.map(c => c.price).filter(p => p > 0);
|
|
179
|
-
const cheapest = Math.min(...competitorPrices);
|
|
180
|
-
const lowStockCount = competitors.filter(c => c.stock !== null && c.stock < 5).length;
|
|
181
|
-
const lowStockRatio = lowStockCount / competitors.length;
|
|
182
|
-
|
|
183
|
-
// Rule: Competitors running out of stock → raise price
|
|
184
|
-
if (lowStockRatio >= 0.7) {
|
|
185
|
-
const newPrice = Math.round(price * 1.05);
|
|
186
|
-
return {
|
|
187
|
-
action: 'INCREASE',
|
|
188
|
-
newPrice,
|
|
189
|
-
reason: `Rakiplerin %${Math.round(lowStockRatio * 100)}'i düşük stokta — %5 fiyat artışı`,
|
|
190
|
-
dryRun: false
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Rule: Be cheaper than cheapest — but respect margin
|
|
195
|
-
let targetPrice = cheapest - 1; // 1 TL cheaper
|
|
196
|
-
|
|
197
|
-
if (strategy === 'aggressive') {
|
|
198
|
-
targetPrice = cheapest - 2; // 2 TL cheaper
|
|
199
|
-
} else if (strategy === 'conservative') {
|
|
200
|
-
targetPrice = cheapest; // match, don't undercut
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Never go below floor
|
|
204
|
-
if (targetPrice < minPrice && minPrice > 0) {
|
|
205
|
-
return {
|
|
206
|
-
action: 'HOLD',
|
|
207
|
-
newPrice: price,
|
|
208
|
-
reason: `Rakip fiyatı (${cheapest} TL) takip edilemiyor — minimum marj %${minMargin} korunamaz.`,
|
|
209
|
-
dryRun: false
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// No change needed
|
|
214
|
-
if (Math.abs(targetPrice - price) < 1) {
|
|
215
|
-
return {
|
|
216
|
-
action: 'HOLD',
|
|
217
|
-
newPrice: price,
|
|
218
|
-
reason: 'Fiyat zaten optimal seviyede',
|
|
219
|
-
dryRun: false
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const action = targetPrice < price ? 'DECREASE' : 'INCREASE';
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
action,
|
|
227
|
-
newPrice: Math.round(targetPrice),
|
|
228
|
-
reason: `Rakip: ${cheapest} TL → Yeni fiyat: ${Math.round(targetPrice)} TL (${action === 'DECREASE' ? 'düşürme' : 'artırma'})`,
|
|
229
|
-
dryRun: false
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Execute a pricing decision through the Critical Lane.
|
|
235
|
-
* @param {object} product - Unified product.
|
|
236
|
-
* @param {object} decision - From calculatePrice().
|
|
237
|
-
* @param {function} updateFn - Async function to apply price on platform.
|
|
238
|
-
*/
|
|
239
|
-
async executeDecision(product, decision, updateFn) {
|
|
240
|
-
if (decision.action === 'HOLD' || decision.action === 'KILL_SWITCH' || decision.action === 'BLOCKED') {
|
|
241
|
-
this._logDecision(product, decision);
|
|
242
|
-
return decision;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Enqueue through Critical Lane for serial execution
|
|
246
|
-
const result = await this.queue.enqueue(
|
|
247
|
-
`Fiyat ${decision.action}: ${product.barcode} → ${decision.newPrice} TL`,
|
|
248
|
-
async () => {
|
|
249
|
-
return await updateFn(product.barcode, decision.newPrice);
|
|
250
|
-
},
|
|
251
|
-
{ priority: 'normal', dryRun: decision.dryRun }
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
this._logDecision(product, decision, result);
|
|
255
|
-
return { ...decision, result };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Get recent pricing decisions.
|
|
260
|
-
*/
|
|
261
|
-
getRecentDecisions(limit = 20) {
|
|
262
|
-
return this.decisions.slice(-limit);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Get engine status.
|
|
267
|
-
*/
|
|
268
|
-
getStatus() {
|
|
269
|
-
return {
|
|
270
|
-
rules: this.rules,
|
|
271
|
-
killSwitch: { active: killSwitchActive, reason: killSwitchReason },
|
|
272
|
-
recentDecisions: this.decisions.length,
|
|
273
|
-
queueStatus: this.queue.getStatus()
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ─────────────────────────────────────────────────────────────────────
|
|
278
|
-
|
|
279
|
-
_logDecision(product, decision, result = null) {
|
|
280
|
-
const entry = {
|
|
281
|
-
barcode: product.barcode,
|
|
282
|
-
oldPrice: product.price,
|
|
283
|
-
action: decision.action,
|
|
284
|
-
newPrice: decision.newPrice,
|
|
285
|
-
reason: decision.reason,
|
|
286
|
-
result: result ? { success: result.success } : null,
|
|
287
|
-
timestamp: new Date().toISOString()
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
this.decisions.push(entry);
|
|
291
|
-
if (this.decisions.length > 200) {
|
|
292
|
-
this.decisions = this.decisions.slice(-200);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
log('INFO', `Pricing decision: ${decision.action} ${product.barcode}`, entry);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
let engineInstance = null;
|
|
300
|
-
|
|
301
|
-
export function getPricingEngine() {
|
|
302
|
-
if (!engineInstance) {
|
|
303
|
-
engineInstance = new PricingEngine();
|
|
304
|
-
}
|
|
305
|
-
return engineInstance;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export default PricingEngine;
|
|
1
|
+
// modules/war-room/pricing-engine.js
|
|
2
|
+
// Dynamic Pricing Engine for Vantuz OS V2
|
|
3
|
+
// Reads rules from BRAND.md, applies pricing strategies, includes KILL SWITCH.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { log } from '../../core/ai-provider.js';
|
|
8
|
+
import { getCriticalQueue } from '../../core/queue.js';
|
|
9
|
+
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// BRAND RULES PARSER
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
|
|
14
|
+
function parseBrandRules() {
|
|
15
|
+
const brandPath = path.join(process.cwd(), 'workspace', 'BRAND.md');
|
|
16
|
+
const defaults = {
|
|
17
|
+
minMargin: 15,
|
|
18
|
+
maxDiscount: 30,
|
|
19
|
+
killSwitchMargin: 5, // Kill switch activates below this
|
|
20
|
+
strategy: 'smart' // 'aggressive', 'smart', 'conservative'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if (!fs.existsSync(brandPath)) return defaults;
|
|
25
|
+
const content = fs.readFileSync(brandPath, 'utf-8');
|
|
26
|
+
|
|
27
|
+
// Parse min margin
|
|
28
|
+
const marginMatch = content.match(/Minimum Kar Marjı[:\s]*%?(\d+)/i);
|
|
29
|
+
if (marginMatch) defaults.minMargin = parseInt(marginMatch[1], 10);
|
|
30
|
+
|
|
31
|
+
// Parse max discount
|
|
32
|
+
const discountMatch = content.match(/Maksimum İndirim[:\s]*%?(\d+)/i);
|
|
33
|
+
if (discountMatch) defaults.maxDiscount = parseInt(discountMatch[1], 10);
|
|
34
|
+
|
|
35
|
+
// Parse strategy
|
|
36
|
+
if (content.includes('Agresif') || content.includes('aggressive')) {
|
|
37
|
+
defaults.strategy = 'aggressive';
|
|
38
|
+
} else if (content.includes('Muhafazakar') || content.includes('conservative')) {
|
|
39
|
+
defaults.strategy = 'conservative';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log('INFO', 'Brand rules parsed', defaults);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
log('WARN', 'Brand rules parse error, using defaults', { error: e.message });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return defaults;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
|
+
// KILL SWITCH
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
|
+
|
|
54
|
+
let killSwitchActive = false;
|
|
55
|
+
let killSwitchReason = '';
|
|
56
|
+
const killSwitchCallbacks = []; // webhook/notification callbacks
|
|
57
|
+
|
|
58
|
+
function activateKillSwitch(reason, productInfo) {
|
|
59
|
+
killSwitchActive = true;
|
|
60
|
+
killSwitchReason = reason;
|
|
61
|
+
|
|
62
|
+
log('ERROR', `🛑 KILL SWITCH ACTIVATED: ${reason}`, productInfo);
|
|
63
|
+
|
|
64
|
+
// Notify all registered callbacks
|
|
65
|
+
for (const cb of killSwitchCallbacks) {
|
|
66
|
+
try { cb({ reason, productInfo, timestamp: new Date().toISOString() }); }
|
|
67
|
+
catch (e) { /* swallow */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resetKillSwitch() {
|
|
72
|
+
killSwitchActive = false;
|
|
73
|
+
killSwitchReason = '';
|
|
74
|
+
log('INFO', '✅ Kill Switch reset — otonom mod devam ediyor');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
78
|
+
// PRICING ENGINE
|
|
79
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
80
|
+
|
|
81
|
+
class PricingEngine {
|
|
82
|
+
constructor() {
|
|
83
|
+
this.rules = parseBrandRules();
|
|
84
|
+
this.queue = getCriticalQueue();
|
|
85
|
+
this.decisions = []; // Recent decisions log
|
|
86
|
+
log('INFO', 'PricingEngine initialized', this.rules);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a callback for Kill Switch activation.
|
|
91
|
+
*/
|
|
92
|
+
onKillSwitch(callback) {
|
|
93
|
+
killSwitchCallbacks.push(callback);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reload brand rules (e.g., after BRAND.md edit).
|
|
98
|
+
*/
|
|
99
|
+
reloadRules() {
|
|
100
|
+
this.rules = parseBrandRules();
|
|
101
|
+
log('INFO', 'Brand rules reloaded', this.rules);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check kill switch status.
|
|
106
|
+
*/
|
|
107
|
+
isKillSwitchActive() {
|
|
108
|
+
return { active: killSwitchActive, reason: killSwitchReason };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Reset kill switch (manual override).
|
|
113
|
+
*/
|
|
114
|
+
resetKillSwitch() {
|
|
115
|
+
resetKillSwitch();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Calculate optimal price for a product.
|
|
120
|
+
* @param {object} product - Unified Product from catalog.
|
|
121
|
+
* @param {object[]} competitors - From CompetitorTracker.
|
|
122
|
+
* @returns {{ action, newPrice, reason, dryRun }}
|
|
123
|
+
*/
|
|
124
|
+
calculatePrice(product, competitors = []) {
|
|
125
|
+
const { price, cost, barcode } = product;
|
|
126
|
+
const { minMargin, killSwitchMargin, strategy } = this.rules;
|
|
127
|
+
|
|
128
|
+
// ── Kill Switch Check ──
|
|
129
|
+
if (cost > 0 && price > 0) {
|
|
130
|
+
const currentMargin = ((price - cost) / price) * 100;
|
|
131
|
+
if (currentMargin < killSwitchMargin) {
|
|
132
|
+
activateKillSwitch(
|
|
133
|
+
`Kar marjı %${currentMargin.toFixed(1)} — %${killSwitchMargin} sınırının altına düştü`,
|
|
134
|
+
{ barcode, price, cost, margin: currentMargin }
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
action: 'KILL_SWITCH',
|
|
138
|
+
newPrice: price,
|
|
139
|
+
reason: `🛑 KILL SWITCH: Marj %${currentMargin.toFixed(1)}. Manuel onay gerekiyor.`,
|
|
140
|
+
dryRun: false
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── If kill switch is globally active ──
|
|
146
|
+
if (killSwitchActive) {
|
|
147
|
+
return {
|
|
148
|
+
action: 'BLOCKED',
|
|
149
|
+
newPrice: price,
|
|
150
|
+
reason: `🛑 Kill Switch aktif: ${killSwitchReason}. resetKillSwitch() ile açın.`,
|
|
151
|
+
dryRun: false
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Price Floor ──
|
|
156
|
+
const minPrice = cost > 0 ? cost * (1 + minMargin / 100) : 0;
|
|
157
|
+
|
|
158
|
+
// ── No Competitors? ──
|
|
159
|
+
if (competitors.length === 0) {
|
|
160
|
+
if (strategy === 'aggressive') {
|
|
161
|
+
const newPrice = Math.round(price * 1.10); // +10%
|
|
162
|
+
return {
|
|
163
|
+
action: 'INCREASE',
|
|
164
|
+
newPrice,
|
|
165
|
+
reason: 'Tek satıcısın — %10 artış uygula (elastiklik testi)',
|
|
166
|
+
dryRun: false
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
action: 'HOLD',
|
|
171
|
+
newPrice: price,
|
|
172
|
+
reason: 'Tek satıcısın — fiyat korunuyor (muhafazakar mod)',
|
|
173
|
+
dryRun: false
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Competitor Analysis ──
|
|
178
|
+
const competitorPrices = competitors.map(c => c.price).filter(p => p > 0);
|
|
179
|
+
const cheapest = Math.min(...competitorPrices);
|
|
180
|
+
const lowStockCount = competitors.filter(c => c.stock !== null && c.stock < 5).length;
|
|
181
|
+
const lowStockRatio = lowStockCount / competitors.length;
|
|
182
|
+
|
|
183
|
+
// Rule: Competitors running out of stock → raise price
|
|
184
|
+
if (lowStockRatio >= 0.7) {
|
|
185
|
+
const newPrice = Math.round(price * 1.05);
|
|
186
|
+
return {
|
|
187
|
+
action: 'INCREASE',
|
|
188
|
+
newPrice,
|
|
189
|
+
reason: `Rakiplerin %${Math.round(lowStockRatio * 100)}'i düşük stokta — %5 fiyat artışı`,
|
|
190
|
+
dryRun: false
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Rule: Be cheaper than cheapest — but respect margin
|
|
195
|
+
let targetPrice = cheapest - 1; // 1 TL cheaper
|
|
196
|
+
|
|
197
|
+
if (strategy === 'aggressive') {
|
|
198
|
+
targetPrice = cheapest - 2; // 2 TL cheaper
|
|
199
|
+
} else if (strategy === 'conservative') {
|
|
200
|
+
targetPrice = cheapest; // match, don't undercut
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Never go below floor
|
|
204
|
+
if (targetPrice < minPrice && minPrice > 0) {
|
|
205
|
+
return {
|
|
206
|
+
action: 'HOLD',
|
|
207
|
+
newPrice: price,
|
|
208
|
+
reason: `Rakip fiyatı (${cheapest} TL) takip edilemiyor — minimum marj %${minMargin} korunamaz.`,
|
|
209
|
+
dryRun: false
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// No change needed
|
|
214
|
+
if (Math.abs(targetPrice - price) < 1) {
|
|
215
|
+
return {
|
|
216
|
+
action: 'HOLD',
|
|
217
|
+
newPrice: price,
|
|
218
|
+
reason: 'Fiyat zaten optimal seviyede',
|
|
219
|
+
dryRun: false
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const action = targetPrice < price ? 'DECREASE' : 'INCREASE';
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
action,
|
|
227
|
+
newPrice: Math.round(targetPrice),
|
|
228
|
+
reason: `Rakip: ${cheapest} TL → Yeni fiyat: ${Math.round(targetPrice)} TL (${action === 'DECREASE' ? 'düşürme' : 'artırma'})`,
|
|
229
|
+
dryRun: false
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Execute a pricing decision through the Critical Lane.
|
|
235
|
+
* @param {object} product - Unified product.
|
|
236
|
+
* @param {object} decision - From calculatePrice().
|
|
237
|
+
* @param {function} updateFn - Async function to apply price on platform.
|
|
238
|
+
*/
|
|
239
|
+
async executeDecision(product, decision, updateFn) {
|
|
240
|
+
if (decision.action === 'HOLD' || decision.action === 'KILL_SWITCH' || decision.action === 'BLOCKED') {
|
|
241
|
+
this._logDecision(product, decision);
|
|
242
|
+
return decision;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Enqueue through Critical Lane for serial execution
|
|
246
|
+
const result = await this.queue.enqueue(
|
|
247
|
+
`Fiyat ${decision.action}: ${product.barcode} → ${decision.newPrice} TL`,
|
|
248
|
+
async () => {
|
|
249
|
+
return await updateFn(product.barcode, decision.newPrice);
|
|
250
|
+
},
|
|
251
|
+
{ priority: 'normal', dryRun: decision.dryRun }
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
this._logDecision(product, decision, result);
|
|
255
|
+
return { ...decision, result };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get recent pricing decisions.
|
|
260
|
+
*/
|
|
261
|
+
getRecentDecisions(limit = 20) {
|
|
262
|
+
return this.decisions.slice(-limit);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get engine status.
|
|
267
|
+
*/
|
|
268
|
+
getStatus() {
|
|
269
|
+
return {
|
|
270
|
+
rules: this.rules,
|
|
271
|
+
killSwitch: { active: killSwitchActive, reason: killSwitchReason },
|
|
272
|
+
recentDecisions: this.decisions.length,
|
|
273
|
+
queueStatus: this.queue.getStatus()
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
_logDecision(product, decision, result = null) {
|
|
280
|
+
const entry = {
|
|
281
|
+
barcode: product.barcode,
|
|
282
|
+
oldPrice: product.price,
|
|
283
|
+
action: decision.action,
|
|
284
|
+
newPrice: decision.newPrice,
|
|
285
|
+
reason: decision.reason,
|
|
286
|
+
result: result ? { success: result.success } : null,
|
|
287
|
+
timestamp: new Date().toISOString()
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
this.decisions.push(entry);
|
|
291
|
+
if (this.decisions.length > 200) {
|
|
292
|
+
this.decisions = this.decisions.slice(-200);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
log('INFO', `Pricing decision: ${decision.action} ${product.barcode}`, entry);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let engineInstance = null;
|
|
300
|
+
|
|
301
|
+
export function getPricingEngine() {
|
|
302
|
+
if (!engineInstance) {
|
|
303
|
+
engineInstance = new PricingEngine();
|
|
304
|
+
}
|
|
305
|
+
return engineInstance;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export default PricingEngine;
|