|
|
|
|
@@ -587,6 +587,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
maintenanceLevel: 50,
|
|
|
|
|
statusFit: 0,
|
|
|
|
|
monthlyBaseCost: 0,
|
|
|
|
|
scandalExtraDailyPct: 0,
|
|
|
|
|
monthsUnderfunded: 0,
|
|
|
|
|
active: true,
|
|
|
|
|
acknowledged: false,
|
|
|
|
|
@@ -3514,6 +3515,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
maintenanceLevel: state.maintenanceLevel,
|
|
|
|
|
statusFit: state.statusFit,
|
|
|
|
|
monthlyBaseCost: state.monthlyBaseCost,
|
|
|
|
|
scandalExtraDailyPct: state.scandalExtraDailyPct,
|
|
|
|
|
monthsUnderfunded: state.monthsUnderfunded,
|
|
|
|
|
active: state.active,
|
|
|
|
|
acknowledged: state.acknowledged,
|
|
|
|
|
@@ -3649,6 +3651,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
statusFit: state.statusFit,
|
|
|
|
|
monthlyBaseCost: monthlyCost,
|
|
|
|
|
monthlyCost,
|
|
|
|
|
scandalExtraDailyPct: state.scandalExtraDailyPct,
|
|
|
|
|
acknowledged: state.acknowledged,
|
|
|
|
|
active: state.active,
|
|
|
|
|
monthsUnderfunded: state.monthsUnderfunded,
|
|
|
|
|
@@ -6889,7 +6892,7 @@ class FalukantService extends BaseService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [products, knowledges, townWorths] = await Promise.all([
|
|
|
|
|
ProductType.findAll({ attributes: ['id', 'sellCost'] }),
|
|
|
|
|
ProductType.findAll({ attributes: ['id', 'sellCost', 'category', 'productionTime'] }),
|
|
|
|
|
Knowledge.findAll({
|
|
|
|
|
where: { characterId: character.id },
|
|
|
|
|
attributes: ['productId', 'knowledge']
|
|
|
|
|
@@ -6900,22 +6903,63 @@ class FalukantService extends BaseService {
|
|
|
|
|
})
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const knowledgeMap = new Map(knowledges.map(k => [k.productId, k.knowledge || 0]));
|
|
|
|
|
const maxWorthByProduct = new Map();
|
|
|
|
|
for (const tw of townWorths) {
|
|
|
|
|
const w = tw.worthPercent ?? 50;
|
|
|
|
|
const prev = maxWorthByProduct.get(tw.productId);
|
|
|
|
|
maxWorthByProduct.set(tw.productId, prev == null ? w : Math.max(prev, w));
|
|
|
|
|
}
|
|
|
|
|
const knowledgeMap = new Map(knowledges.map((k) => [k.productId, k.knowledge || 0]));
|
|
|
|
|
const worthByProductRegion = new Map(
|
|
|
|
|
townWorths.map((tw) => [`${tw.productId}-${tw.regionId}`, tw.worthPercent ?? 50])
|
|
|
|
|
);
|
|
|
|
|
const certificate = Number(user.certificate || 1);
|
|
|
|
|
const taxByRegion = new Map();
|
|
|
|
|
await Promise.all(
|
|
|
|
|
worthRegionIds.map(async (rid) => {
|
|
|
|
|
taxByRegion.set(rid, await getCumulativeTaxPercentWithExemptions(user.id, rid));
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const prices = {};
|
|
|
|
|
const netMetrics = {};
|
|
|
|
|
for (const product of products) {
|
|
|
|
|
const worthPercent = maxWorthByProduct.get(product.id) ?? 50;
|
|
|
|
|
const knowledgeFactor = knowledgeMap.get(product.id) || 0;
|
|
|
|
|
const price = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
|
|
|
|
|
if (price !== null) prices[product.id] = price;
|
|
|
|
|
const productionTime = Number(product.productionTime || 0);
|
|
|
|
|
const pieceCost = productionPieceCost(certificate, product.category);
|
|
|
|
|
let best = null;
|
|
|
|
|
for (const rid of worthRegionIds) {
|
|
|
|
|
const worthPercent = worthByProductRegion.get(`${product.id}-${rid}`) ?? 50;
|
|
|
|
|
const grossPrice = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
|
|
|
|
|
if (grossPrice == null) continue;
|
|
|
|
|
const taxPercent = Number(taxByRegion.get(rid) || 0);
|
|
|
|
|
const transportPerPiece = networkWorth && rid !== regionId ? (grossPrice * 0.01) : 0;
|
|
|
|
|
const taxableProfit = Math.max(0, grossPrice - pieceCost - transportPerPiece);
|
|
|
|
|
const taxPerPiece = taxableProfit * (taxPercent / 100);
|
|
|
|
|
const netPerPiece = grossPrice - pieceCost - taxPerPiece - transportPerPiece;
|
|
|
|
|
const netPerMinute = productionTime > 0 ? (netPerPiece / productionTime) : 0;
|
|
|
|
|
const candidate = {
|
|
|
|
|
regionId: rid,
|
|
|
|
|
grossPrice,
|
|
|
|
|
netPerPiece,
|
|
|
|
|
netPerMinute,
|
|
|
|
|
pieceCost,
|
|
|
|
|
taxPercent,
|
|
|
|
|
taxPerPiece,
|
|
|
|
|
transportPerPiece,
|
|
|
|
|
};
|
|
|
|
|
if (!best || candidate.netPerMinute > best.netPerMinute) {
|
|
|
|
|
best = candidate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!best) continue;
|
|
|
|
|
prices[product.id] = best.grossPrice;
|
|
|
|
|
netMetrics[product.id] = {
|
|
|
|
|
regionId: best.regionId,
|
|
|
|
|
grossPerPiece: Number(best.grossPrice.toFixed(2)),
|
|
|
|
|
netPerPiece: Number(best.netPerPiece.toFixed(2)),
|
|
|
|
|
netPerMinute: Number(best.netPerMinute.toFixed(2)),
|
|
|
|
|
pieceCost: Number(best.pieceCost.toFixed(2)),
|
|
|
|
|
taxPercent: Number(best.taxPercent.toFixed(2)),
|
|
|
|
|
taxPerPiece: Number(best.taxPerPiece.toFixed(2)),
|
|
|
|
|
transportPerPiece: Number(best.transportPerPiece.toFixed(2)),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { prices };
|
|
|
|
|
return { prices, netMetrics };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[getAllProductPricesInRegion] Error for regionId=${regionId}:`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
@@ -7061,7 +7105,10 @@ ORDER BY r.id`,
|
|
|
|
|
const { cities, branchTypeByCityId } = citiesWithBranchType;
|
|
|
|
|
|
|
|
|
|
const [products, knowledges, townWorths] = await Promise.all([
|
|
|
|
|
ProductType.findAll({ where: { id: { [Op.in]: productIds } }, attributes: ['id', 'sellCost'] }),
|
|
|
|
|
ProductType.findAll({
|
|
|
|
|
where: { id: { [Op.in]: productIds } },
|
|
|
|
|
attributes: ['id', 'sellCost', 'category', 'productionTime']
|
|
|
|
|
}),
|
|
|
|
|
Knowledge.findAll({
|
|
|
|
|
where: { characterId: characterId, productId: { [Op.in]: productIds } },
|
|
|
|
|
attributes: ['productId', 'knowledge']
|
|
|
|
|
@@ -7075,44 +7122,76 @@ ORDER BY r.id`,
|
|
|
|
|
const knowledgeByProduct = new Map(knowledges.map(k => [k.productId, k.knowledge || 0]));
|
|
|
|
|
const worthByProductRegion = new Map(townWorths.map(tw => [`${tw.productId}-${tw.regionId}`, tw.worthPercent]));
|
|
|
|
|
|
|
|
|
|
const PRICE_TOLERANCE = 0.01;
|
|
|
|
|
const certificate = Number(user.certificate || 1);
|
|
|
|
|
const taxByCity = new Map();
|
|
|
|
|
await Promise.all(
|
|
|
|
|
cities.map(async (city) => {
|
|
|
|
|
taxByCity.set(city.id, await getCumulativeTaxPercentWithExemptions(user.id, city.id));
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const PRICE_TOLERANCE = 0.0001;
|
|
|
|
|
const out = {};
|
|
|
|
|
|
|
|
|
|
for (const product of products) {
|
|
|
|
|
const knowledgeFactor = knowledgeByProduct.get(product.id) || 0;
|
|
|
|
|
const pieceCost = productionPieceCost(certificate, product.category);
|
|
|
|
|
const productionTime = Number(product.productionTime || 0);
|
|
|
|
|
const clientPriceRaw = priceByProduct.get(product.id);
|
|
|
|
|
const clientPriceNum = clientPriceRaw != null && clientPriceRaw !== ''
|
|
|
|
|
? Number(clientPriceRaw)
|
|
|
|
|
: NaN;
|
|
|
|
|
let currentRegionalPrice;
|
|
|
|
|
let currentRegionalNetPerMinute;
|
|
|
|
|
// Wie getProductPricesInCities: bei bekannter Standort-Region immer
|
|
|
|
|
// serverseitigen Verkaufspreis dieser Region als Referenz — nicht den
|
|
|
|
|
// Client-Wert (Ertrags-Tabelle kann MAX-Worth über Filialen nutzen).
|
|
|
|
|
if (currentRegionId) {
|
|
|
|
|
const wp = worthByProductRegion.get(`${product.id}-${currentRegionId}`) ?? 50;
|
|
|
|
|
currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp)
|
|
|
|
|
const grossCurrent = calcRegionalSellPriceSync(product, knowledgeFactor, wp)
|
|
|
|
|
?? (!Number.isNaN(clientPriceNum) ? clientPriceNum : Number(product.sellCost) ?? 0);
|
|
|
|
|
const taxPercentCurrent = Number(taxByCity.get(currentRegionId) || 0);
|
|
|
|
|
const taxableProfitCurrent = Math.max(0, grossCurrent - pieceCost);
|
|
|
|
|
const taxCurrent = taxableProfitCurrent * (taxPercentCurrent / 100);
|
|
|
|
|
const netPieceCurrent = grossCurrent - pieceCost - taxCurrent;
|
|
|
|
|
currentRegionalNetPerMinute = productionTime > 0 ? (netPieceCurrent / productionTime) : 0;
|
|
|
|
|
} else if (!Number.isNaN(clientPriceNum)) {
|
|
|
|
|
currentRegionalPrice = clientPriceNum;
|
|
|
|
|
const taxPercentCurrent = Number(taxByCity.get(currentRegionId) || 0);
|
|
|
|
|
const taxableProfitCurrent = Math.max(0, clientPriceNum - pieceCost);
|
|
|
|
|
const taxCurrent = taxableProfitCurrent * (taxPercentCurrent / 100);
|
|
|
|
|
const netPieceCurrent = clientPriceNum - pieceCost - taxCurrent;
|
|
|
|
|
currentRegionalNetPerMinute = productionTime > 0 ? (netPieceCurrent / productionTime) : 0;
|
|
|
|
|
} else {
|
|
|
|
|
currentRegionalPrice = Number(product.sellCost) || 0;
|
|
|
|
|
const fallbackGross = Number(product.sellCost) || 0;
|
|
|
|
|
const netPieceCurrent = fallbackGross - pieceCost;
|
|
|
|
|
currentRegionalNetPerMinute = productionTime > 0 ? (netPieceCurrent / productionTime) : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
for (const city of cities) {
|
|
|
|
|
if (currentRegionId && city.id === currentRegionId) continue;
|
|
|
|
|
const worthPercent = worthByProductRegion.get(`${product.id}-${city.id}`) ?? 50;
|
|
|
|
|
const priceInCity = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
|
|
|
|
|
if (priceInCity == null) continue;
|
|
|
|
|
if (priceInCity <= currentRegionalPrice - PRICE_TOLERANCE) continue;
|
|
|
|
|
const grossPrice = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent);
|
|
|
|
|
if (grossPrice == null) continue;
|
|
|
|
|
const taxPercent = Number(taxByCity.get(city.id) || 0);
|
|
|
|
|
const transportPerPiece = currentRegionId && city.id !== currentRegionId ? (grossPrice * 0.01) : 0;
|
|
|
|
|
const taxableProfit = Math.max(0, grossPrice - pieceCost - transportPerPiece);
|
|
|
|
|
const taxPerPiece = taxableProfit * (taxPercent / 100);
|
|
|
|
|
const netPerPiece = grossPrice - pieceCost - taxPerPiece - transportPerPiece;
|
|
|
|
|
const netPerMinute = productionTime > 0 ? (netPerPiece / productionTime) : 0;
|
|
|
|
|
if (netPerMinute <= currentRegionalNetPerMinute + PRICE_TOLERANCE) continue;
|
|
|
|
|
results.push({
|
|
|
|
|
regionId: city.id,
|
|
|
|
|
regionName: city.name,
|
|
|
|
|
price: priceInCity,
|
|
|
|
|
branchType: branchTypeByCityId.get(city.id) ?? null
|
|
|
|
|
price: Number(grossPrice.toFixed(2)),
|
|
|
|
|
grossPerMinute: Number((productionTime > 0 ? grossPrice / productionTime : 0).toFixed(2)),
|
|
|
|
|
netPerPiece: Number(netPerPiece.toFixed(2)),
|
|
|
|
|
netPerMinute: Number(netPerMinute.toFixed(2)),
|
|
|
|
|
taxPercent: Number(taxPercent.toFixed(2)),
|
|
|
|
|
transportPerPiece: Number(transportPerPiece.toFixed(2)),
|
|
|
|
|
branchType: branchTypeByCityId.get(city.id) ?? null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
results.sort((a, b) => b.price - a.price);
|
|
|
|
|
results.sort((a, b) => b.netPerMinute - a.netPerMinute);
|
|
|
|
|
out[product.id] = results;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|