From 2fb440f03341f8bbe6704b69e11aa7fbe4301d65 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sat, 20 Dec 2025 15:37:16 +0100 Subject: [PATCH] Implement synchronous price calculation for batch operations in FalukantService, optimizing performance by reducing database queries. Update inventory handling to batch delete items and enhance revenue calculations. Fix translation formatting in German locale for sellAllSuccess message. --- backend/services/falukantService.js | 130 ++++++++++++++++++--- frontend/src/i18n/locales/de/falukant.json | 2 +- 2 files changed, 114 insertions(+), 18 deletions(-) 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",