Refactor sellAllProducts method in FalukantService to ensure atomic transactions for selling products, updating inventory, and handling financial transactions. Implement batch processing for sell items and enhance error handling for inventory deletions. Update updateFalukantUserMoney function to support transactions, improving consistency and reliability in financial operations.
This commit is contained in:
@@ -1679,133 +1679,148 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sellAllProducts(hashedUserId, branchId) {
|
async sellAllProducts(hashedUserId, branchId) {
|
||||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
// Konsistenz-Garantie: Verkauf, DaySell-Log, Geldbuchung und Inventory-Löschung müssen atomar sein.
|
||||||
const branch = await Branch.findOne({
|
// Sonst kann es (wie beobachtet) zu "teilweise verkauft/gelöscht" kommen.
|
||||||
where: { id: branchId, falukantUserId: falukantUser.id },
|
return await sequelize.transaction(async (t) => {
|
||||||
include: [{ model: FalukantStock, as: 'stocks' }]
|
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||||
});
|
const branch = await Branch.findOne({
|
||||||
if (!branch) throw new Error('Branch not found');
|
where: { id: branchId, falukantUserId: falukantUser.id },
|
||||||
const stockIds = branch.stocks.map(s => s.id);
|
include: [{ model: FalukantStock, as: 'stocks' }],
|
||||||
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id } });
|
transaction: t
|
||||||
if (!character) throw new Error('No character for user');
|
|
||||||
const inventory = await Inventory.findAll({
|
|
||||||
where: { stockId: stockIds },
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: ProductType,
|
|
||||||
as: 'productType',
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Knowledge,
|
|
||||||
as: 'knowledges',
|
|
||||||
required: false,
|
|
||||||
where: {
|
|
||||||
characterId: character.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: FalukantStock,
|
|
||||||
as: 'stock',
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Branch,
|
|
||||||
as: 'branch'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: FalukantStockType,
|
|
||||||
as: 'stockType'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
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 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
|
|
||||||
});
|
});
|
||||||
}
|
if (!branch) throw new Error('Branch not found');
|
||||||
|
const stockIds = branch.stocks.map(s => s.id);
|
||||||
|
const character = await FalukantCharacter.findOne({ where: { userId: falukantUser.id }, transaction: t });
|
||||||
|
if (!character) throw new Error('No character for user');
|
||||||
|
const inventory = await Inventory.findAll({
|
||||||
|
where: { stockId: stockIds },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ProductType,
|
||||||
|
as: 'productType',
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Knowledge,
|
||||||
|
as: 'knowledges',
|
||||||
|
required: false,
|
||||||
|
where: {
|
||||||
|
characterId: character.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: FalukantStock,
|
||||||
|
as: 'stock',
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Branch,
|
||||||
|
as: 'branch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: FalukantStockType,
|
||||||
|
as: 'stockType'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
if (!inventory.length) return { success: true, revenue: 0 };
|
||||||
|
|
||||||
const totalNet = Math.round((total - totalTax) * 100) / 100;
|
// 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))];
|
||||||
|
|
||||||
// 5. Batch-Update DaySell Einträge
|
// 1. Batch-Load alle TownProductWorth Einträge
|
||||||
await this.addSellItemsBatch(sellItems, falukantUser.id);
|
const townWorths = await TownProductWorth.findAll({
|
||||||
|
where: {
|
||||||
|
productId: { [Op.in]: productIds },
|
||||||
|
regionId: { [Op.in]: regionIds }
|
||||||
|
},
|
||||||
|
transaction: t
|
||||||
|
});
|
||||||
|
const worthMap = new Map();
|
||||||
|
townWorths.forEach(tw => {
|
||||||
|
worthMap.set(`${tw.productId}-${tw.regionId}`, tw.worthPercent);
|
||||||
|
});
|
||||||
|
|
||||||
const moneyResult = await updateFalukantUserMoney(
|
// 2/3. Batch-Berechne Steuern für alle Regionen
|
||||||
falukantUser.id,
|
const taxMap = new Map();
|
||||||
totalNet,
|
const uniqueRegionIds = [...new Set(regionIds)];
|
||||||
'Sell all products (net)',
|
for (const regionId of uniqueRegionIds) {
|
||||||
falukantUser.id
|
const tax = await getCumulativeTaxPercentWithExemptions(falukantUser.id, regionId);
|
||||||
);
|
taxMap.set(regionId, tax);
|
||||||
if (!moneyResult.success) throw new Error('Failed to update money for seller');
|
}
|
||||||
|
|
||||||
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
|
// 4. Berechne Preise, Steuern und Einnahmen in EINER Schleife
|
||||||
if (treasuryId && totalTax > 0) {
|
let total = 0;
|
||||||
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), Math.round(totalTax * 100) / 100, `Sales tax (aggregate)`, falukantUser.id);
|
let totalTax = 0;
|
||||||
if (!taxResult.success) throw new Error('Failed to update money for treasury');
|
const sellItems = [];
|
||||||
}
|
|
||||||
|
for (const item of inventory) {
|
||||||
// Batch-Delete Inventory Items
|
const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0;
|
||||||
const inventoryIds = inventory.map(item => item.id);
|
const regionId = item.stock.branch.regionId;
|
||||||
await Inventory.destroy({ where: { id: { [Op.in]: inventoryIds } } });
|
const worthPercent = worthMap.get(`${item.productType.id}-${regionId}`) || 50;
|
||||||
|
|
||||||
console.log('[FalukantService.sellAllProducts] emitting events for user', falukantUser.user.hashedId, 'branch', branchId, 'revenue', total, 'items', inventory.length);
|
const pricePerUnit = calcRegionalSellPriceSync(item.productType, knowledgeVal, worthPercent);
|
||||||
notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {});
|
|
||||||
notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', { branchId });
|
const cumulativeTax = taxMap.get(regionId);
|
||||||
return { success: true, revenue: total };
|
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;
|
||||||
|
|
||||||
|
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 (innerhalb Transaktion)
|
||||||
|
await this.addSellItemsBatch(sellItems, falukantUser.id, t);
|
||||||
|
|
||||||
|
// 6. Inventory löschen (innerhalb Transaktion) und sicherstellen, dass alles weg ist
|
||||||
|
const inventoryIds = inventory.map(item => item.id).filter(Boolean);
|
||||||
|
const expected = inventoryIds.length;
|
||||||
|
const deleted = await Inventory.destroy({ where: { id: { [Op.in]: inventoryIds } }, transaction: t });
|
||||||
|
if (deleted !== expected) {
|
||||||
|
throw new Error(`Inventory delete mismatch: expected ${expected}, deleted ${deleted}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Geld buchen (innerhalb Transaktion)
|
||||||
|
const moneyResult = await updateFalukantUserMoney(
|
||||||
|
falukantUser.id,
|
||||||
|
totalNet,
|
||||||
|
'Sell all products (net)',
|
||||||
|
falukantUser.id,
|
||||||
|
t
|
||||||
|
);
|
||||||
|
if (!moneyResult.success) throw new Error('Failed to update money for seller');
|
||||||
|
|
||||||
|
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
|
||||||
|
if (treasuryId && totalTax > 0) {
|
||||||
|
const taxResult = await updateFalukantUserMoney(
|
||||||
|
parseInt(treasuryId, 10),
|
||||||
|
Math.round(totalTax * 100) / 100,
|
||||||
|
`Sales tax (aggregate)`,
|
||||||
|
falukantUser.id,
|
||||||
|
t
|
||||||
|
);
|
||||||
|
if (!taxResult.success) throw new Error('Failed to update money for treasury');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[FalukantService.sellAllProducts] sold items', expected, 'deleted', deleted, 'revenue', total);
|
||||||
|
notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {});
|
||||||
|
notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', { branchId });
|
||||||
|
return { success: true, revenue: total };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSellItem(branchId, userId, productId, quantity) {
|
async addSellItem(branchId, userId, productId, quantity) {
|
||||||
@@ -1833,14 +1848,15 @@ class FalukantService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSellItemsBatch(sellItems, userId) {
|
async addSellItemsBatch(sellItems, userId, transaction = null) {
|
||||||
if (!sellItems || sellItems.length === 0) return;
|
if (!sellItems || sellItems.length === 0) return;
|
||||||
|
|
||||||
// Lade alle benötigten Branches auf einmal
|
// Lade alle benötigten Branches auf einmal
|
||||||
const branchIds = [...new Set(sellItems.map(item => item.branchId))];
|
const branchIds = [...new Set(sellItems.map(item => item.branchId))];
|
||||||
const branches = await Branch.findAll({
|
const branches = await Branch.findAll({
|
||||||
where: { id: { [Op.in]: branchIds } },
|
where: { id: { [Op.in]: branchIds } },
|
||||||
attributes: ['id', 'regionId']
|
attributes: ['id', 'regionId'],
|
||||||
|
transaction: transaction || undefined
|
||||||
});
|
});
|
||||||
const branchMap = new Map(branches.map(b => [b.id, b]));
|
const branchMap = new Map(branches.map(b => [b.id, b]));
|
||||||
// WICHTIG: Wie bei addSellItem muss ein fehlender Branch ein harter Fehler sein,
|
// WICHTIG: Wie bei addSellItem muss ein fehlender Branch ein harter Fehler sein,
|
||||||
@@ -1883,11 +1899,12 @@ class FalukantService extends BaseService {
|
|||||||
productId: data.productId,
|
productId: data.productId,
|
||||||
sellerId: data.sellerId
|
sellerId: data.sellerId
|
||||||
},
|
},
|
||||||
defaults: { quantity: data.quantity }
|
defaults: { quantity: data.quantity },
|
||||||
|
transaction: transaction || undefined
|
||||||
}).then(([daySell, created]) => {
|
}).then(([daySell, created]) => {
|
||||||
if (!created) {
|
if (!created) {
|
||||||
daySell.quantity += data.quantity;
|
daySell.quantity += data.quantity;
|
||||||
return daySell.save();
|
return daySell.save({ transaction: transaction || undefined });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ const updateSchema = async (models) => {
|
|||||||
console.log('✅ Datenbankschema aktualisiert');
|
console.log('✅ Datenbankschema aktualisiert');
|
||||||
};
|
};
|
||||||
|
|
||||||
async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null) {
|
async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null, transaction = null) {
|
||||||
try {
|
try {
|
||||||
const result = await sequelize.query(
|
const result = await sequelize.query(
|
||||||
`SELECT falukant_data.update_money(
|
`SELECT falukant_data.update_money(
|
||||||
@@ -418,6 +418,7 @@ async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, ch
|
|||||||
changedBy,
|
changedBy,
|
||||||
},
|
},
|
||||||
type: sequelize.QueryTypes.SELECT,
|
type: sequelize.QueryTypes.SELECT,
|
||||||
|
transaction: transaction || undefined,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user