vantuz 3.4.2 β 3.5.1
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/.env.example +21 -0
- package/.openclaw/completions/openclaw.bash +227 -0
- package/.openclaw/completions/openclaw.fish +1552 -0
- package/.openclaw/completions/openclaw.ps1 +1966 -0
- package/.openclaw/completions/openclaw.zsh +3571 -0
- package/.openclaw/gateway.cmd +10 -0
- package/.openclaw/identity/device.json +7 -0
- package/.openclaw/openclaw.json +40 -0
- package/.windsurf/workflows/vantuz-dev.md +31 -0
- package/DOCS_TR.md +80 -0
- package/LICENSE +45 -45
- package/README.md +52 -21
- 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/n11docs.md +1680 -0
- package/nodes/warehouse.js +238 -238
- package/onboard.js +1 -1
- package/openclawdocs.md +3 -0
- 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/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
- package/workspace/AGENTS.md +73 -0
- package/workspace/BRAND.md +29 -0
- package/workspace/SOUL.md +72 -0
- package/workspace/team/DECISIONS.md +3 -0
- package/workspace/team/GOALS.md +3 -0
- package/workspace/team/PROJECT_STATUS.md +3 -0
- package/workspace/team/agents/dev/SOUL.md +12 -0
- package/workspace/team/agents/josh/SOUL.md +12 -0
- package/workspace/team/agents/marketing/SOUL.md +12 -0
- package/workspace/team/agents/milo/SOUL.md +12 -0
- package/vantuz-3.3.4.tgz +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;
|