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:
@@ -72,9 +72,10 @@ function calcAge(birthdate) {
|
||||
return differenceInDays(now, b);
|
||||
}
|
||||
|
||||
async function getFalukantUserOrFail(hashedId) {
|
||||
async function getFalukantUserOrFail(hashedId, options = {}) {
|
||||
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');
|
||||
return user;
|
||||
@@ -1682,7 +1683,7 @@ class FalukantService extends BaseService {
|
||||
// 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.
|
||||
return await sequelize.transaction(async (t) => {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId, { transaction: t });
|
||||
const branch = await Branch.findOne({
|
||||
where: { id: branchId, falukantUserId: falukantUser.id },
|
||||
include: [{ model: FalukantStock, as: 'stocks' }],
|
||||
@@ -3281,70 +3282,124 @@ class FalukantService extends BaseService {
|
||||
}
|
||||
|
||||
async createParty(hashedUserId, partyTypeId, musicId, banquetteId, nobilityIds = [], servantRatio) {
|
||||
const falukantUser = await getFalukantUserOrFail(hashedUserId);
|
||||
const since = new Date(Date.now() - 24 * 3600 * 1000);
|
||||
const already = await Party.findOne({
|
||||
where: {
|
||||
falukantUserId: falukantUser.id,
|
||||
// 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 since = new Date(Date.now() - 24 * 3600 * 1000);
|
||||
const already = await Party.findOne({
|
||||
where: {
|
||||
falukantUserId: falukantUser.id,
|
||||
partyTypeId,
|
||||
createdAt: { [Op.gte]: since },
|
||||
},
|
||||
attributes: ['id'],
|
||||
transaction: t
|
||||
});
|
||||
if (already) {
|
||||
throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt');
|
||||
}
|
||||
|
||||
const [ptype, music, banquette] = await Promise.all([
|
||||
PartyType.findByPk(partyTypeId, { transaction: t }),
|
||||
MusicType.findByPk(musicId, { transaction: t }),
|
||||
BanquetteType.findByPk(banquetteId, { transaction: t }),
|
||||
]);
|
||||
if (!ptype || !music || !banquette) {
|
||||
throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl');
|
||||
}
|
||||
|
||||
const nobilities = nobilityIds && nobilityIds.length
|
||||
? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } }, transaction: t })
|
||||
: [];
|
||||
|
||||
// Prüfe, ob alle angegebenen IDs gefunden wurden
|
||||
if (nobilityIds && nobilityIds.length > 0 && nobilities.length !== nobilityIds.length) {
|
||||
throw new Error('Einige ausgewählte Adelstitel existieren nicht');
|
||||
}
|
||||
|
||||
let cost = (ptype.cost || 0) + (music.cost || 0) + (banquette.cost || 0);
|
||||
cost += (50 / servantRatio - 1) * 1000;
|
||||
const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0);
|
||||
cost += nobilityCost;
|
||||
|
||||
if (Number(falukantUser.money) < cost) {
|
||||
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(
|
||||
falukantUser.id,
|
||||
-cost,
|
||||
'partyOrder',
|
||||
falukantUser.id,
|
||||
t
|
||||
);
|
||||
if (!moneyResult.success) {
|
||||
throw new Error('Geld konnte nicht abgezogen werden');
|
||||
}
|
||||
|
||||
const party = await Party.create({
|
||||
partyTypeId,
|
||||
createdAt: { [Op.gte]: since },
|
||||
},
|
||||
attributes: ['id']
|
||||
falukantUserId: falukantUser.id,
|
||||
musicTypeId: musicId,
|
||||
banquetteTypeId: banquetteId,
|
||||
servantRatio,
|
||||
cost: cost
|
||||
}, { transaction: t });
|
||||
|
||||
if (nobilities.length > 0) {
|
||||
await party.addInvitedNobilities(nobilities, { transaction: t });
|
||||
}
|
||||
|
||||
// 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', {
|
||||
partyId: party.id,
|
||||
cost,
|
||||
reputationGain,
|
||||
});
|
||||
// Statusbar kann sich damit ebenfalls aktualisieren
|
||||
notifyUser(user.hashedId, 'falukantUpdateStatus', {});
|
||||
|
||||
return { success: true, reputationGain };
|
||||
});
|
||||
if (already) {
|
||||
throw new Error('Diese Party wurde bereits innerhalb der letzten 24 Stunden bestellt');
|
||||
}
|
||||
const [ptype, music, banquette] = await Promise.all([
|
||||
PartyType.findByPk(partyTypeId),
|
||||
MusicType.findByPk(musicId),
|
||||
BanquetteType.findByPk(banquetteId),
|
||||
]);
|
||||
if (!ptype || !music || !banquette) {
|
||||
throw new Error('Ungültige Party-, Musik- oder Bankett-Auswahl');
|
||||
}
|
||||
const nobilities = nobilityIds && nobilityIds.length
|
||||
? await TitleOfNobility.findAll({ where: { id: { [Op.in]: nobilityIds } } })
|
||||
: [];
|
||||
|
||||
// Prüfe, ob alle angegebenen IDs gefunden wurden
|
||||
if (nobilityIds && nobilityIds.length > 0 && nobilities.length !== nobilityIds.length) {
|
||||
throw new Error('Einige ausgewählte Adelstitel existieren nicht');
|
||||
}
|
||||
|
||||
let cost = (ptype.cost || 0) + (music.cost || 0) + (banquette.cost || 0);
|
||||
cost += (50 / servantRatio - 1) * 1000;
|
||||
const nobilityCost = nobilities.reduce((sum, n) => sum + ((n.id ^ 5) * 1000), 0);
|
||||
cost += nobilityCost;
|
||||
if (Number(falukantUser.money) < cost) {
|
||||
throw new Error('Nicht genügend Guthaben für diese Party');
|
||||
}
|
||||
const moneyResult = await updateFalukantUserMoney(
|
||||
falukantUser.id,
|
||||
-cost,
|
||||
'partyOrder',
|
||||
falukantUser.id
|
||||
);
|
||||
if (!moneyResult.success) {
|
||||
throw new Error('Geld konnte nicht abgezogen werden');
|
||||
}
|
||||
const party = await Party.create({
|
||||
partyTypeId,
|
||||
falukantUserId: falukantUser.id,
|
||||
musicTypeId: musicId,
|
||||
banquetteTypeId: banquetteId,
|
||||
servantRatio,
|
||||
cost: cost
|
||||
});
|
||||
if (nobilities.length > 0) {
|
||||
// Verwende die bereits geladenen Objekte
|
||||
await party.addInvitedNobilities(nobilities);
|
||||
}
|
||||
const user = await User.findByPk(falukantUser.userId);
|
||||
notifyUser(user.hashedId, 'falukantPartyUpdate', {
|
||||
partyId: party.id,
|
||||
cost,
|
||||
});
|
||||
return { 'success': true };
|
||||
}
|
||||
|
||||
async getParties(hashedUserId) {
|
||||
|
||||
Reference in New Issue
Block a user