diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index 1d10aba..dfe2a25 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -92,6 +92,14 @@ function calcSellPrice(product, knowledgeFactor = 0) { return min + (max - min) * (knowledgeFactor / 100); } +// Synchron version für Batch-Operationen (ohne DB-Query) +function calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent = 50) { + const basePrice = product.sellCost * (worthPercent / 100); + const min = basePrice * 0.6; + const max = basePrice; + return min + (max - min) * (knowledgeFactor / 100); +} + async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) { // Wenn worthPercent nicht übergeben wurde, hole es aus der Datenbank if (worthPercent === null) { @@ -1698,33 +1706,68 @@ class FalukantService extends BaseService { ] }); if (!inventory.length) return { success: true, revenue: 0 }; + + // PERFORMANCE OPTIMIZATION: Batch-Load alle benötigten Daten + const regionIds = [...new Set(inventory.map(item => item.stock.branch.regionId))]; + const productIds = [...new Set(inventory.map(item => item.productType.id))]; + + // 1. Batch-Load alle TownProductWorth Einträge + const townWorths = await TownProductWorth.findAll({ + where: { + productId: { [Op.in]: productIds }, + regionId: { [Op.in]: regionIds } + } + }); + const worthMap = new Map(); + townWorths.forEach(tw => { + worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent); + }); + + // 2. Batch-Load alle RegionData (falls benötigt, aber eigentlich schon in inventory.stock.branch enthalten) + // 3. Batch-Berechne Steuern für alle Regionen + const taxMap = new Map(); + const uniqueRegionIds = [...new Set(regionIds)]; + for (const regionId of uniqueRegionIds) { + const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId); + taxMap.set(regionId, tax); + } + + // 4. Berechne Preise, Steuern und Einnahmen in EINER Schleife let total = 0; + let totalTax = 0; + const sellItems = []; + for (const item of inventory) { const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0; const regionId = item.stock.branch.regionId; - const pricePerUnit = await calcRegionalSellPrice(item.productType, knowledgeVal, regionId); - const cumulativeTax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId); - const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100)); - const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100; - total += item.quantity * adjustedPricePerUnit; - await this.addSellItem(item.stock.branch.id, falukantUser.id, item.productType.id, item.quantity); - } - // compute tax per region (using cumulative tax per region) and aggregate - let totalTax = 0; - for (const item of inventory) { - const regionId = item.stock.branch.regionId; - const region = await RegionData.findOne({ where: { id: regionId } }); - const cumulativeTax = await getCumulativeTaxPercent(regionId); - const pricePerUnit = await calcRegionalSellPrice(item.productType, item.productType.knowledges?.[0]?.knowledge || 0, regionId); + const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50; + + // Berechne Preis (ohne DB-Query, da worthPercent bereits geladen) + const pricePerUnit = calcRegionalSellPriceSync(item.productType, knowledgeVal, worthPercent); + + // Hole Steuer aus Cache/Map + const cumulativeTax = taxMap.get(regionId); const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100)); const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100; const itemRevenue = item.quantity * adjustedPricePerUnit; const itemTax = Math.round((itemRevenue * cumulativeTax / 100) * 100) / 100; + + total += itemRevenue; totalTax += itemTax; + + // Sammle SellItems für Batch-Operation + sellItems.push({ + branchId: item.stock.branch.id, + productId: item.productType.id, + quantity: item.quantity + }); } const totalNet = Math.round((total - totalTax) * 100) / 100; + // 5. Batch-Update DaySell Einträge + await this.addSellItemsBatch(sellItems, falukantUser.id); + const moneyResult = await updateFalukantUserMoney( falukantUser.id, totalNet, @@ -1738,9 +1781,11 @@ class FalukantService extends BaseService { const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), Math.round(totalTax * 100) / 100, `Sales tax (aggregate)`, falukantUser.id); if (!taxResult.success) throw new Error('Failed to update money for treasury'); } - for (const item of inventory) { - await Inventory.destroy({ where: { id: item.id } }); - } + + // Batch-Delete Inventory Items + const inventoryIds = inventory.map(item => item.id); + await Inventory.destroy({ where: { id: { [Op.in]: inventoryIds } } }); + console.log('[FalukantService.sellAllProducts] emitting events for user', falukantUser.user.hashedId, 'branch', branchId, 'revenue', total, 'items', inventory.length); notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {}); notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', { branchId }); @@ -1772,6 +1817,57 @@ class FalukantService extends BaseService { } } + async addSellItemsBatch(sellItems, userId) { + if (!sellItems || sellItems.length === 0) return; + + // Lade alle benötigten Branches auf einmal + const branchIds = [...new Set(sellItems.map(item => item.branchId))]; + const branches = await Branch.findAll({ + where: { id: { [Op.in]: branchIds } }, + attributes: ['id', 'regionId'] + }); + const branchMap = new Map(branches.map(b => [b.id, b])); + + // Gruppiere nach (regionId, productId, sellerId) + const grouped = new Map(); + for (const item of sellItems) { + const branch = branchMap.get(item.branchId); + if (!branch) continue; + + const key = `${branch.regionId}-${item.productId}-${userId}`; + if (!grouped.has(key)) { + grouped.set(key, { + regionId: branch.regionId, + productId: item.productId, + sellerId: userId, + quantity: 0 + }); + } + grouped.get(key).quantity += item.quantity; + } + + // Batch-Update oder Create für alle gruppierten Einträge + const promises = []; + for (const [key, data] of grouped) { + promises.push( + DaySell.findOrCreate({ + where: { + regionId: data.regionId, + productId: data.productId, + sellerId: data.sellerId + }, + defaults: { quantity: data.quantity } + }).then(([daySell, created]) => { + if (!created) { + daySell.quantity += data.quantity; + return daySell.save(); + } + }) + ); + } + await Promise.all(promises); + } + // Return tax summary for a branch: total cumulative tax and breakdown per region (region -> parent chain) async getBranchTaxes(hashedUserId, branchId) { const user = await getFalukantUserOrFail(hashedUserId); diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index a809d42..a5e373b 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -263,7 +263,7 @@ "selling": "Verkauf läuft...", "sellError": "Fehler beim Verkauf des Produkts.", "sellAllError": "Fehler beim Verkauf aller Produkte.", - "sellAllSuccess": "Alle Produkte wurden erfolgreich verkauft. Einnahmen: {revenue}" + "sellAllSuccess": "Alle Produkte wurden erfolgreich verkauft. Einnahmen: {revenue}", "transportTitle": "Transport anlegen", "transportSource": "Artikel", "transportSourcePlaceholder": "Artikel wählen",