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);
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user