Enhance getFalukantUserOrFail and createParty methods in FalukantService to support transaction options

- Updated getFalukantUserOrFail to accept an options parameter for transaction handling.
- Refactored createParty to utilize transaction support, ensuring atomic operations for party creation and related financial transactions.
- Improved error handling for party creation, including checks for existing parties within a 24-hour window and validation of selected options.
This commit is contained in:
Torsten Schulz (local)
2025-12-20 23:30:10 +01:00
parent 6cf8fa8a9c
commit 38f23cc6ae

View File

@@ -72,9 +72,10 @@ function calcAge(birthdate) {
return differenceInDays(now, b); return differenceInDays(now, b);
} }
async function getFalukantUserOrFail(hashedId) { async function getFalukantUserOrFail(hashedId, options = {}) {
const user = await FalukantUser.findOne({ const user = await FalukantUser.findOne({
include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }] include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }],
transaction: options.transaction
}); });
if (!user) throw new Error('User not found'); if (!user) throw new Error('User not found');
return user; return user;
@@ -1682,7 +1683,7 @@ class FalukantService extends BaseService {
// Konsistenz-Garantie: Verkauf, DaySell-Log, Geldbuchung und Inventory-Löschung müssen atomar sein. // Konsistenz-Garantie: Verkauf, DaySell-Log, Geldbuchung und Inventory-Löschung müssen atomar sein.
// Sonst kann es (wie beobachtet) zu "teilweise verkauft/gelöscht" kommen. // Sonst kann es (wie beobachtet) zu "teilweise verkauft/gelöscht" kommen.
return await sequelize.transaction(async (t) => { return await sequelize.transaction(async (t) => {
const falukantUser = await getFalukantUserOrFail(hashedUserId); const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t });
const branch = await Branch.findOne({ const branch = await Branch.findOne({
where: { id: branchId, falukantUserId: falukantUser.id }, where: { id: branchId, falukantUserId: falukantUser.id },
include: [{ model: FalukantStock, as: 'stocks' }], include: [{ model: FalukantStock, as: 'stocks' }],
@@ -3281,6 +3282,10 @@ class FalukantService extends BaseService {
} }
async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) { async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) {
// Reputation-Logik: Party steigert Reputation um 1..5 (bestes Fest 5, kleinstes 1).
// Wir leiten "Ausstattung" aus den Party-Kosten ab (linear zwischen min/max möglicher Konfiguration),
// und deckeln Reputation bei 100.
return await sequelize.transaction(async (t) => {
const falukantUser = await getFalukantUserOrFail(hashedUserId); const falukantUser = await getFalukantUserOrFail(hashedUserId);
const since = new Date(Date.now() - 24 * 3600 * 1000); const since = new Date(Date.now() - 24 * 3600 * 1000);
const already = await Party.findOne({ const already = await Party.findOne({
@@ -3289,21 +3294,24 @@ class FalukantService extends BaseService {
partyTypeId, partyTypeId,
createdAt: { [Op.gte]: since }, createdAt: { [Op.gte]: since },
}, },
attributes: ['id'] attributes: ['id'],
transaction: t
}); });
if (already) { if (already) {
throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt'); throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt');
} }
const [ptype, music, banquette] = await Promise.all([ const [ptype, music, banquette] = await Promise.all([
PartyType.findByPk(partyTypeId), PartyType.findByPk(partyTypeId, { transaction: t }),
MusicType.findByPk(musicId), MusicType.findByPk(musicId, { transaction: t }),
BanquetteType.findByPk(banquetteId), BanquetteType.findByPk(banquetteId, { transaction: t }),
]); ]);
if (!ptype || !music || !banquette) { if (!ptype || !music || !banquette) {
throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl'); throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl');
} }
const nobilities = nobilityIds && nobilityIds.length const nobilities = nobilityIds && nobilityIds.length
? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } } }) ? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } }, transaction: t })
: []; : [];
// Prüfe, ob alle angegebenen IDs gefunden wurden // Prüfe, ob alle angegebenen IDs gefunden wurden
@@ -3315,18 +3323,53 @@ class FalukantService extends BaseService {
cost += (50 / servantRatio - 1) * 1000; cost += (50 / servantRatio - 1) * 1000;
const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0); const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0);
cost += nobilityCost; cost += nobilityCost;
if (Number(falukantUser.money) < cost) { if (Number(falukantUser.money) < cost) {
throw new Error('Nicht genügend Guthaben für diese Party'); throw new Error('Nicht genügend Guthaben für diese Party');
} }
// min/max mögliche Kosten für die Skalierung (nur für Reputation; Party-Preis bleibt wie berechnet)
const [allPartyTypes, allMusicTypes, allBanquetteTypes, allNobilityTitles] = await Promise.all([
PartyType.findAll({ attributes: ['cost'], transaction: t }),
MusicType.findAll({ attributes: ['cost'], transaction: t }),
BanquetteType.findAll({ attributes: ['cost'], transaction: t }),
TitleOfNobility.findAll({ attributes: ['id'], transaction: t }),
]);
const minParty = allPartyTypes.length ? Math.min(...allPartyTypes.map(x => Number(x.cost || 0))) : 0;
const maxParty = allPartyTypes.length ? Math.max(...allPartyTypes.map(x => Number(x.cost || 0))) : 0;
const minMusic = allMusicTypes.length ? Math.min(...allMusicTypes.map(x => Number(x.cost || 0))) : 0;
const maxMusic = allMusicTypes.length ? Math.max(...allMusicTypes.map(x => Number(x.cost || 0))) : 0;
const minBanq = allBanquetteTypes.length ? Math.min(...allBanquetteTypes.map(x => Number(x.cost || 0))) : 0;
const maxBanq = allBanquetteTypes.length ? Math.max(...allBanquetteTypes.map(x => Number(x.cost || 0))) : 0;
const servantsMin = 0; // servantRatio=50 => (50/50 - 1)*1000 = 0
const servantsMax = (50 / 1 - 1) * 1000; // servantRatio=1 => 49k
const nobilityMax = (allNobilityTitles || []).reduce((sum, n) => sum + ((Number(n.id) ^ 5) * 1000), 0);
const minCostPossible = (minParty || 0) + (minMusic || 0) + (minBanq || 0) + servantsMin;
const maxCostPossible = (maxParty || 0) + (maxMusic || 0) + (maxBanq || 0) + servantsMax + (nobilityMax || 0);
const denom = Math.max(1, (maxCostPossible - minCostPossible));
const score = Math.min(1, Math.max(0, (cost - minCostPossible) / denom));
const reputationGain = 1 + Math.round(score * 4); // 1..5
const character = await FalukantCharacter.findOne({
where: { userId: falukantUser.id },
attributes: ['id', 'reputation'],
transaction: t
});
if (!character) throw new Error('No character for user');
// Geld abziehen
const moneyResult = await updateFalukantUserMoney( const moneyResult = await updateFalukantUserMoney(
falukantUser.id, falukantUser.id,
-cost, -cost,
'partyOrder', 'partyOrder',
falukantUser.id falukantUser.id,
t
); );
if (!moneyResult.success) { if (!moneyResult.success) {
throw new Error('Geld konnte nicht abgezogen werden'); throw new Error('Geld konnte nicht abgezogen werden');
} }
const party = await Party.create({ const party = await Party.create({
partyTypeId, partyTypeId,
falukantUserId: falukantUser.id, falukantUserId: falukantUser.id,
@@ -3334,17 +3377,29 @@ class FalukantService extends BaseService {
banquetteTypeId: banquetteId, banquetteTypeId: banquetteId,
servantRatio, servantRatio,
cost: cost cost: cost
}); }, { transaction: t });
if (nobilities.length > 0) { if (nobilities.length > 0) {
// Verwende die bereits geladenen Objekte await party.addInvitedNobilities(nobilities, { transaction: t });
await party.addInvitedNobilities(nobilities);
} }
const user = await User.findByPk(falukantUser.userId);
// Reputation erhöhen (0..100)
await character.update(
{ reputation: Sequelize.literal(`LEAST(100, COALESCE(reputation,0) + ${reputationGain})`) },
{ transaction: t }
);
const user = await User.findByPk(falukantUser.userId, { transaction: t });
notifyUser(user.hashedId, 'falukantPartyUpdate', { notifyUser(user.hashedId, 'falukantPartyUpdate', {
partyId: party.id, partyId: party.id,
cost, cost,
reputationGain,
});
// Statusbar kann sich damit ebenfalls aktualisieren
notifyUser(user.hashedId, 'falukantUpdateStatus', {});
return { success: true, reputationGain };
}); });
return { 'success': true };
} }
async getParties(hashedUserId) { async getParties(hashedUserId) {