import BaseService from './BaseService.js'; import { Sequelize, Op } from 'sequelize'; import FalukantPredefineFirstname from '../models/falukant/predefine/firstname.js'; import FalukantPredefineLastname from '../models/falukant/predefine/lastname.js'; import FalukantUser from '../models/falukant/data/user.js'; import FalukantCharacter from '../models/falukant/data/character.js'; import RegionData from '../models/falukant/data/region.js'; import RegionType from '../models/falukant/type/region.js'; import FalukantStock from '../models/falukant/data/stock.js'; import FalukantStockType from '../models/falukant/type/stock.js'; import TitleOfNobility from '../models/falukant/type/title_of_nobility.js'; import Branch from '../models/falukant/data/branch.js'; import BranchType from '../models/falukant/type/branch.js'; import Production from '../models/falukant/data/production.js'; import ProductType from '../models/falukant/type/product.js'; import Knowledge from '../models/falukant/data/product_knowledge.js'; import Inventory from '../models/falukant/data/inventory.js'; import MoneyFlow from '../models/falukant/log/moneyflow.js'; import User from '../models/community/user.js'; import { notifyUser } from '../utils/socket.js'; import { differenceInDays } from 'date-fns'; import { updateFalukantUserMoney } from '../utils/sequelize.js'; import BuyableStock from '../models/falukant/data/buyable_stock.js'; import DirectorProposal from '../models/falukant/data/director_proposal.js'; import Director from '../models/falukant/data/director.js'; import DaySell from '../models/falukant/log/daysell.js'; function calcAge(birthdate) { const b = new Date(birthdate); b.setHours(0, 0); const now = new Date(); now.setHours(0, 0); return differenceInDays(now, b); } async function getFalukantUserOrFail(hashedId) { const user = await FalukantUser.findOne({ include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }] }); if (!user) throw new Error('User not found'); return user; } async function getBranchOrFail(userId, branchId) { const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: userId } }); if (!branch) throw new Error('Branch not found'); return branch; } function calcSellPrice(product, knowledgeFactor = 0) { const max = product.sellCost; const min = max * 0.6; return min + (max - min) * (knowledgeFactor / 100); } class FalukantService extends BaseService { async getFalukantUserByHashedId(hashedId) { return FalukantUser.findOne({ include: [{ model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId } }] }); } async getUser(hashedUserId) { const u = await FalukantUser.findOne({ include: [ { model: User, as: 'user', attributes: ['username', 'hashedId'], where: { hashedId: hashedUserId } }, { model: FalukantCharacter, as: 'character', include: [ { model: FalukantPredefineFirstname, as: 'definedFirstName', attributes: ['name'] }, { model: FalukantPredefineLastname, as: 'definedLastName', attributes: ['name'] }, { model: TitleOfNobility, as: 'nobleTitle', attributes: ['labelTr'] } ], attributes: ['birthdate', 'gender'] }, { model: RegionData, as: 'mainBranchRegion', include: [{ model: RegionType, as: 'regionType' }], attributes: ['name'] }, { model: Branch, as: 'branches', include: [ { model: BranchType, as: 'branchType', attributes: ['labelTr'] }, { model: RegionData, as: 'region', include: [{ model: RegionType, as: 'regionType' }], attributes: ['name'] } ] } ], attributes: ['money', 'creditAmount', 'todayCreditTaken'] }); if (!u) throw new Error('User not found'); if (u.character?.birthdate) u.character.setDataValue('age', calcAge(u.character.birthdate)); return u; } async randomFirstName(gender) { const names = await FalukantPredefineFirstname.findAll({ where: { gender } }); return names[Math.floor(Math.random() * names.length)].name; } async randomLastName() { const names = await FalukantPredefineLastname.findAll(); return names[Math.floor(Math.random() * names.length)].name; } async createUser(hashedUserId, gender, firstName, lastName) { const user = await this.getUserByHashedId(hashedUserId); if (await FalukantUser.findOne({ where: { userId: user.id } })) throw new Error('User already exists in Falukant.'); let fnObj = await FalukantPredefineFirstname.findOne({ where: { name: firstName } }); let lnObj = await FalukantPredefineLastname.findOne({ where: { name: lastName } }); if (!fnObj) fnObj = await FalukantPredefineFirstname.create({ name: firstName, gender }); if (!lnObj) lnObj = await FalukantPredefineLastname.create({ name: lastName }); const region = await RegionData.findOne({ order: Sequelize.fn('RANDOM'), limit: 1, include: [{ model: RegionType, as: 'regionType', where: { labelTr: 'city' } }] }); if (!region) throw new Error('No region found with the label "city".'); const nobility = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } }); if (!nobility) throw new Error('No title of nobility found with the label "noncivil".'); const falukantUser = await FalukantUser.create({ userId: user.id, money: 50, creditAmount: 0, todayCreditTaken: 0, creditInterestRate: 0, mainBranchRegionId: region.id }); const date = new Date(); date.setDate(date.getDate() - 14); const ch = await FalukantCharacter.create({ userId: falukantUser.id, regionId: region.id, firstName: fnObj.id, lastName: lnObj.id, gender, birthdate: date, titleOfNobility: nobility.id }); const stType = await FalukantStockType.findOne({ where: [{ label_tr: 'wood' }] }); await FalukantStock.create({ userId: falukantUser.id, regionId: region.id, stockTypeId: stType.id, quantity: 10 }); falukantUser.character = ch; const bType = await BranchType.findOne({ where: { labelTr: 'fullstack' } }); await Branch.create({ userId: falukantUser.id, regionId: region.id, branchTypeId: bType.id }); notifyUser(user.hashedId, 'reloadmenu', {}); return falukantUser; } async getInfo(hashedUserId) { const user = await User.findOne({ where: { hashedId: hashedUserId } }); if (!user) throw new Error('User not found'); const falukantUser = await FalukantUser.findOne({ include: [ { model: User, as: 'user', attributes: ['hashedId'], where: { hashedId: hashedUserId } }, { model: FalukantCharacter, as: 'character', attributes: ['birthdate', 'health'] } ], attributes: ['money'] }); if (!falukantUser) throw new Error('User not found'); if (falukantUser.character?.birthdate) falukantUser.character.setDataValue('age', calcAge(falukantUser.character.birthdate)); return falukantUser; } async getBranches(hashedUserId) { const u = await getFalukantUserOrFail(hashedUserId); const bs = await Branch.findAll({ where: { falukantUserId: u.id }, include: [ { model: BranchType, as: 'branchType', attributes: ['labelTr'] }, { model: RegionData, as: 'region', attributes: ['name'] } ], attributes: ['id', 'regionId'], order: [['branchTypeId', 'ASC']] }); return bs.map(b => ({ ...b.toJSON(), isMainBranch: u.mainBranchRegionId === b.regionId })); } async getBranch(hashedUserId, branchId) { const u = await getFalukantUserOrFail(hashedUserId); const br = await Branch.findOne({ where: { id: branchId, falukantUserId: u.id }, include: [ { model: BranchType, as: 'branchType', attributes: ['labelTr'] }, { model: RegionData, as: 'region', attributes: ['name'] }, { model: Production, as: 'productions', attributes: ['quantity', 'startTimestamp'], include: [{ model: ProductType, as: 'productType', attributes: ['id', 'category', 'labelTr', 'sellCost', 'productionTime'] }] } ], attributes: ['id', 'regionId'] }); if (!br) throw new Error('Branch not found'); return br; } async getStock(hashedUserId, branchId) { const u = await getFalukantUserOrFail(hashedUserId); const b = await getBranchOrFail(u.id, branchId); return FalukantStock.findAll({ where: { regionId: b.regionId, userId: u.id } }); } async createStock(hashedUserId, branchId, stockData) { const u = await getFalukantUserOrFail(hashedUserId); const b = await getBranchOrFail(u.id, branchId); return FalukantStock.create({ userId: u.id, regionId: b.regionId, stockTypeId: stockData.stockTypeId, quantity: stockData.quantity }); } async createProduction(hashedUserId, branchId, productId, quantity) { const u = await getFalukantUserOrFail(hashedUserId); const b = await getBranchOrFail(u.id, branchId); const p = await ProductType.findOne({ where: { id: productId } }); if (!p) throw new Error('Product not found'); const cost = quantity * p.category * 6; if (u.money < cost) throw new Error('notenoughmoney'); const r = await updateFalukantUserMoney(u.id, -cost, 'Production cost', u.id); if (!r.success) throw new Error('Failed to update money'); const d = await Production.create({ branchId: b.id, productId, quantity }); notifyUser(u.user.hashedId, 'falukantUpdateStatus', {}); notifyUser(u.user.hashedId, 'falukantBranchUpdate', { branchId: b.id }); return d; } async getProduction(hashedUserId, branchId) { const u = await getFalukantUserOrFail(hashedUserId); const b = await getBranchOrFail(u.id, branchId); return Production.findOne({ where: { regionId: b.regionId } }); } async getProducts(hashedUserId) { const u = await getFalukantUserOrFail(hashedUserId); console.log(u); const c = await FalukantCharacter.findOne({ where: { userId: u.id } }); console.log(c); if (!c) { throw new Error(`No FalukantCharacter found for user with id ${u.id}`); } const ps = await ProductType.findAll({ where: { category: { [Op.lte]: u.certificate } }, include: [{ model: Knowledge, as: 'knowledges', attributes: ['knowledge'], where: { characterId: c.id } }], attributes: ['labelTr', 'id', 'sellCost', 'productionTime', 'category'] }); return ps; } async getInventory(hashedUserId, branchId) { const u = await getFalukantUserOrFail(hashedUserId); const f = branchId ? { id: branchId, falukantUserId: u.id } : { falukantUserId: u.id }; const br = await Branch.findAll({ where: f, include: [ { model: FalukantStock, as: 'stocks', include: [{ model: FalukantStockType, as: 'stockType' }] }, { model: RegionData, as: 'region', include: [{ model: RegionType, as: 'regionType' }] } ] }); const stockIds = br.flatMap(b => b.stocks.map(s => s.id)); const inv = await Inventory.findAll({ where: { stockId: stockIds }, include: [ { model: FalukantStock, as: 'stock', include: [ { model: Branch, as: 'branch', include: [{ model: RegionData, as: 'region', include: [{ model: RegionType, as: 'regionType' }] }] }, { model: FalukantStockType, as: 'stockType' } ] }, { model: ProductType, as: 'productType' } ] }); const grouped = inv.reduce((acc, i) => { const r = i.stock.branch.region; const k = `${r.id}-${i.productType.id}-${i.quality}`; acc[k] = acc[k] || { region: r, product: i.productType, quality: i.quality, totalQuantity: 0 }; acc[k].totalQuantity += i.quantity; return acc; }, {}); return Object.values(grouped).sort((a, b) => { if (a.region.id !== b.region.id) return a.region.id - b.region.id; if (a.product.id !== b.product.id) return a.product.id - b.product.id; return a.quality - b.quality; }); } async sellProduct(hashedUserId, branchId, productId, quality, quantity) { const user = await getFalukantUserOrFail(hashedUserId); const branch = await getBranchOrFail(user.id, branchId); const character = await FalukantCharacter.findOne({ where: { userId: user.id } }); if (!character) throw new Error('No character found for user'); const stock = await FalukantStock.findOne({ where: { branchId: branch.id } }); if (!stock) throw new Error('Stock not found'); const inventory = await Inventory.findAll({ where: { stockId: stock.id, quality }, include: [ { model: ProductType, as: 'productType', required: true, where: { id: productId }, include: [ { model: Knowledge, as: 'knowledges', required: false, where: { characterId: character.id } } ] } ] }); if (!inventory.length) throw new Error('No inventory found'); const available = inventory.reduce((sum, i) => sum + i.quantity, 0); if (available < quantity) throw new Error('Not enough inventory available'); const item = inventory[0].productType; const knowledgeVal = item.knowledges?.[0]?.knowledge || 0; const revenue = quantity * calcSellPrice(item, knowledgeVal); const moneyResult = await updateFalukantUserMoney(user.id, revenue, 'Product sale', user.id); if (!moneyResult.success) throw new Error('Failed to update money'); let remaining = quantity; for (const inv of inventory) { if (inv.quantity <= remaining) { remaining -= inv.quantity; await inv.destroy(); } else { await inv.update({ quantity: inv.quantity - remaining }); remaining = 0; break; } } await this.addSellItem(branchId, falukantUser.id, productId, quantity); notifyUser(user.user.hashedId, 'falukantUpdateStatus', {}); notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id }); return { success: true }; } async sellAllProducts(hashedUserId, branchId) { const falukantUser = await getFalukantUserOrFail(hashedUserId); const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: falukantUser.id }, include: [{ model: FalukantStock, as: 'stocks' }] }); 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 } }); 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 }; let total = 0; for (const item of inventory) { const knowledgeVal = item.productType.knowledges[0]?.knowledge || 0; total += item.quantity * calcSellPrice(item.productType, knowledgeVal); await this.addSellItem(item.stock[0].branch[0].id, falukantUser.id, item.productType.id, item.quantity); } const moneyResult = await updateFalukantUserMoney( falukantUser.id, total, 'Sell all products', falukantUser.id ); if (!moneyResult.success) throw new Error('Failed to update money'); for (const item of inventory) { await Inventory.destroy({ where: { id: item.id } }); } notifyUser(falukantUser.user.hashedId, 'falukantUpdateStatus', {}); notifyUser(falukantUser.user.hashedId, 'falukantBranchUpdate', {}); return { success: true, revenue: total }; } async addSellItem(branchId, userId, productId, quantity) { const branch = await Branch.findOne({ where: { id: branchId }, }) ; const daySell = await DaySell.findOne({ where: { regionId: regionId, productId: productId, sellerId: userId, } }); if (daySell) { daySell.quantity += quantity; await daySell.save(); } else { await DaySell.create({ regionId: regionId, productId: productId, sellerId: userId, quantity: quantity, }); } } async moneyHistory(hashedUserId, page = 1, filter = '') { const u = await getFalukantUserOrFail(hashedUserId); const limit = 25, offset = (page - 1) * limit; const w = { falukantUserId: u.id }; if (filter) w.activity = { [Op.iLike]: `%${filter}%` }; const { rows, count } = await MoneyFlow.findAndCountAll({ where: w, order: [['time', 'DESC']], limit, offset }); return { data: rows, total: count, currentPage: page, totalPages: Math.ceil(count / limit) }; } async getStorage(hashedUserId, branchId) { const user = await getFalukantUserOrFail(hashedUserId); const branch = await getBranchOrFail(user.id, branchId); const stocks = await FalukantStock.findAll({ where: { branchId: branch.id }, include: [{ model: FalukantStockType, as: 'stockType' }], }); const stockIds = stocks.map(s => s.id); const inventoryItems = await Inventory.findAll({ where: { stockId: stockIds }, include: [ { model: FalukantStock, as: 'stock', include: [{ model: FalukantStockType, as: 'stockType' }], }, ], }); let totalUsedCapacity = 0; const usageByType = {}; for (const s of stocks) { const stId = s.stockTypeId; if (!usageByType[stId]) { usageByType[stId] = { stockTypeId: stId, stockTypeLabelTr: s.stockType?.labelTr || `stockType:${stId}`, totalCapacity: 0, used: 0 }; } usageByType[stId].totalCapacity += s.quantity; } for (const item of inventoryItems) { totalUsedCapacity += item.quantity; const stId = item.stock.stockTypeId; if (!usageByType[stId]) { usageByType[stId] = { stockTypeId: stId, stockTypeLabelTr: item.stock.stockType?.labelTr || `stockType:${stId}`, totalCapacity: 0, used: 0 }; } usageByType[stId].used += item.quantity; } const buyableStocks = await BuyableStock.findAll({ where: { regionId: branch.regionId }, include: [{ model: FalukantStockType, as: 'stockType' }], }); const buyableByType = {}; for (const b of buyableStocks) { const stId = b.stockTypeId; if (!buyableByType[stId]) { buyableByType[stId] = { stockTypeId: stId, stockTypeLabelTr: b.stockType?.labelTr || `stockType:${stId}`, quantity: 0 }; } buyableByType[stId].quantity += b.quantity; } let maxCapacity = stocks.reduce((sum, s) => sum + s.quantity, 0); return { branchId, totalUsedCapacity, maxCapacity, usageByType: Object.values(usageByType), buyableByType: Object.values(buyableByType) }; } async buyStorage(hashedUserId, branchId, amount, stockTypeId) { const user = await getFalukantUserOrFail(hashedUserId); const branch = await getBranchOrFail(user.id, branchId); const buyable = await BuyableStock.findOne({ where: { regionId: branch.regionId, stockTypeId }, include: [{ model: FalukantStockType, as: 'stockType' }] }); if (!buyable || buyable.quantity < amount) throw new Error('Not enough buyable stock'); const costPerUnit = buyable.stockType.cost; const totalCost = costPerUnit * amount; if (user.money < totalCost) throw new Error('notenoughmoney'); const moneyResult = await updateFalukantUserMoney( user.id, -totalCost, `Buy storage (type: ${buyable.stockType.labelTr})`, user.id ); if (!moneyResult.success) throw new Error('Failed to update money'); buyable.quantity -= amount; await buyable.save(); const stock = await FalukantStock.findOne({ where: { branchId: branch.id, stockTypeId }, include: [{ model: FalukantStockType, as: 'stockType' }] }); if (!stock) throw new Error('No stock record found for this branch and stockType'); stock.quantity += amount; await stock.save(); return { success: true, bought: amount, totalCost, stockType: buyable.stockType.labelTr }; } async sellStorage(hashedUserId, branchId, amount, stockTypeId) { const user = await getFalukantUserOrFail(hashedUserId); const branch = await getBranchOrFail(user.id, branchId); const stock = await FalukantStock.findOne({ where: { branchId: branch.id, stockTypeId }, include: [{ model: FalukantStockType, as: 'stockType' }] }); if (!stock || stock.quantity < amount) throw new Error('Not enough stock to sell'); const costPerUnit = stock.stockType.cost; const totalRevenue = costPerUnit * amount; const moneyResult = await updateFalukantUserMoney( user.id, totalRevenue, `Sell storage (type: ${stock.stockType.labelTr})`, user.id ); if (!moneyResult.success) throw new Error('Failed to update money'); stock.quantity -= amount; await stock.save(); const buyable = await BuyableStock.findOne({ where: { regionId: branch.regionId, stockTypeId }, include: [{ model: FalukantStockType, as: 'stockType' }] }); if (!buyable) throw new Error('No buyable record found for this region and stockType'); buyable.quantity += amount; await buyable.save(); return { success: true, sold: amount, totalRevenue, stockType: stock.stockType.labelTr }; } async notifyRegionUsersAboutStockChange(regionId) { const users = await FalukantCharacter.findAll({ where: { regionId: regionId }, include: [ { model: FalukantUser, as: 'user', include: [ { model: User, as: 'user' }, { model: Branch, as: 'branch', where: { regionId: regionId } } ] } ] }); for (const user of users) { notifyUser(user.user[0].user[0].hashedId, 'stock_change', { branchId: user.user[0].branch[0].id }); } } async getStockTypes() { return FalukantStockType.findAll(); } async getStockOverview() { const items = await Inventory.findAll({ include: [ { model: FalukantStock, as: 'stock', include: [ { model: Branch, as: 'branch', include: [{ model: RegionData, as: 'region' }] } ] }, { model: ProductType, as: 'productType' } ] }); const result = items.map(inv => ({ regionName: inv.stock?.branch?.region?.name || '???', productLabelTr: inv.productType?.labelTr || '???', quantity: inv.quantity })); return result; } async getAllProductions(hashedUserId) { const user = await getFalukantUserOrFail(hashedUserId); const productions = await Production.findAll({ include: [ { model: Branch, as: 'branch', include: [ { model: RegionData, as: 'region', attributes: ['name'] } ], where: { falukantUserId: user.id } }, { model: ProductType, as: 'productType', attributes: ['labelTr', 'productionTime'] } ], attributes: ['startTimestamp', 'quantity'], }); const formattedProductions = productions.map((production) => { const startTimestamp = new Date(production.startTimestamp).getTime(); const endTimestamp = startTimestamp + production.productType.productionTime * 60 * 1000; return { cityName: production.branch.region.name, productName: production.productType.labelTr, quantity: production.quantity, endTimestamp: new Date(endTimestamp).toISOString(), }; }); formattedProductions.sort((a, b) => new Date(a.endTimestamp) - new Date(b.endTimestamp)); return formattedProductions; } async getDirectorProposals(hashedUserId, branchId) { const user = await this.getFalukantUserByHashedId(hashedUserId); if (!user) { throw new Error('User not found'); } const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id } }); if (!branch) { throw new Error('Branch not found or does not belong to the user'); } const { falukantUserId, regionId } = branch; await this.deleteExpiredProposals(); const existingProposals = await this.fetchProposals(falukantUserId, regionId); if (existingProposals.length > 0) { return this.formatProposals(existingProposals); } await this.generateProposals(falukantUserId, regionId); const newProposals = await this.fetchProposals(falukantUserId, regionId); return this.formatProposals(newProposals); } async deleteExpiredProposals() { const expirationTime = new Date(Date.now() - 24 * 60 * 60 * 1000); await DirectorProposal.destroy({ where: { createdAt: { [Op.lt]: expirationTime, }, }, }); } async fetchProposals(falukantUserId, regionId) { return DirectorProposal.findAll({ where: { employerUserId: falukantUserId }, include: [ { model: FalukantCharacter, as: 'character', attributes: ['firstName', 'lastName', 'birthdate', 'titleOfNobility', 'gender'], where: { regionId }, include: [ { model: FalukantPredefineFirstname, as: 'definedFirstName' }, { model: FalukantPredefineLastname, as: 'definedLastName' }, { model: TitleOfNobility, as: 'nobleTitle' }, { model: Knowledge, as: 'knowledges', include: [ { model: ProductType, as: 'productType' }, ] }, ], }, ], }); } async generateProposals(falukantUserId, regionId) { const proposalCount = Math.floor(Math.random() * 3) + 3; for (let i = 0; i < proposalCount; i++) { const directorCharacter = await FalukantCharacter.findOne({ where: { regionId, createdAt: { [Op.lt]: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000), }, }, order: Sequelize.fn('RANDOM'), }); if (!directorCharacter) { throw new Error('No directors available for the region'); } const avgKnowledge = await this.calculateAverageKnowledge(directorCharacter.id); const proposedIncome = Math.round( directorCharacter.titleOfNobility * Math.pow(1.231, avgKnowledge / 1.5) ); await DirectorProposal.create({ directorCharacterId: directorCharacter.id, employerUserId: falukantUserId, proposedIncome, }); } } async calculateAverageKnowledge(characterId) { const averageKnowledge = await Knowledge.findAll({ where: { characterId }, attributes: [[Sequelize.fn('AVG', Sequelize.col('knowledge')), 'avgKnowledge']], raw: true, }); return parseFloat(averageKnowledge[0]?.avgKnowledge || 0); } formatProposals(proposals) { return proposals.map((proposal) => { const age = Math.floor((Date.now() - new Date(proposal.character.birthdate)) / (24 * 60 * 60 * 1000)); const knowledge = proposal.character.knowledges?.map(k => ({ productId: k.productId, value: k.knowledge, labelTr: k.productType.labelTr, })) || []; return { id: proposal.id, proposedIncome: proposal.proposedIncome, character: { name: `${proposal.character.definedFirstName.name} ${proposal.character.definedLastName.name}`, title: proposal.character.nobleTitle.labelTr, age, knowledge, gender: proposal.character.gender, }, }; }); } async convertProposalToDirector(hashedUserId, proposalId) { const user = await getFalukantUserOrFail(hashedUserId); if (!user) { throw new Error('User not found'); } const proposal = await DirectorProposal.findOne( { where: { id: proposalId }, include: [ { model: FalukantCharacter, as: 'character' }, ] } ); if (!proposal || proposal.employerUserId !== user.id) { throw new Error('Proposal does not belong to the user'); } const existingDirector = await Director.findOne({ where: { employerUserId: user.id }, include: [ { model: FalukantCharacter, as: 'character', where: { regionId: proposal.character.regionId, } }, ] }); if (existingDirector) { throw new Error('A director already exists for this region'); } const newDirector = await Director.create({ directorCharacterId: proposal.directorCharacterId, employerUserId: proposal.employerUserId, income: proposal.proposedIncome, }); const regionUserDirectorProposals = await DirectorProposal.findAll({ where: { employerUserId: proposal.employerUserId, }, include: [ { model: FalukantCharacter, as: 'character', where: { regionId: proposal.character.regionId, } }, ] }); if (regionUserDirectorProposals.length > 0) { for (const proposal of regionUserDirectorProposals) { await DirectorProposal.destroy(); } } notifyUser(hashedUserId, 'directorchanged'); return newDirector; } async getDirectorForBranch(hashedUserId, branchId) { const user = await getFalukantUserOrFail(hashedUserId); if (!user) { throw new Error('User not found'); } const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id } }); if (!branch) { throw new Error('Branch not found or does not belong to the user'); } const director = await Director.findOne({ where: { employerUserId: user.id }, include: [ { model: FalukantCharacter, as: 'character', attributes: ['firstName', 'lastName', 'birthdate', 'titleOfNobility', 'gender'], where: { regionId: branch.regionId, }, include: [ { model: TitleOfNobility, as: 'nobleTitle' }, { model: FalukantPredefineFirstname, as: 'definedFirstName' }, { model: FalukantPredefineLastname, as: 'definedLastName' }, ] }, ], }); if (!director) { return null; } const age = Math.floor((Date.now() - new Date(director.character.birthdate)) / (24 * 60 * 60 * 1000)); return { director: { id: director.id, character: { name: `${director.character.definedFirstName.name} ${director.character.definedLastName.name}`, title: director.character.nobleTitle.labelTr, age, gender: director.character.gender, }, income: director.income, satisfaction: director.satisfaction, mayProduce: director.mayProduce, maySell: director.maySell, mayStartTransport: director.mayStartTransport, }, }; } async setSetting(hashedUserId, branchId, directorId, settingKey, value) { const user = await this.getFalukantUserByHashedId(hashedUserId); const branch = await Branch.findOne({ where: { id: branchId, falukantUserId: user.id, }, }); if (!branch) { return null; } const director = await Director.findOne({ where: { id: directorId, employerUserId: user.id, }, include: [{ model: FalukantCharacter, as: 'character', where: { regionId: branch.regionId, } }] }); if (!director) { return null; } const updateData = {}; updateData[settingKey] = value || false; await Director.update(updateData, { where: { id: director.id, }, }); return { result: 'ok' }; } async getMarriageProposals(hashedUserId) { const user = await this.getFalukantUserByHashedId(hashedUserId); const character = await FalukantCharacter.findOne({ where: { userId: user.id } }); if (!character) { throw new Error('Character not found for this user'); } const midnight = new Date(); midnight.setHours(0, 0, 0, 0); await MarriageProposal.destroy({ where: { [Op.or]: [ { requesterCharacterId: character.id }, { proposedCharacterId: character.id }, ], createdAt: { [Op.lt]: midnight, }, }, }); let proposals = await MarriageProposal.findAll({ where: { [Op.or]: [ { requesterCharacterId: character.id }, { proposedCharacterId: character.id }, ], }, }); if (proposals.length === 0) { const proposalCount = Math.floor(Math.random() * 4) + 3; // 3–6 const thirteenDaysAgo = new Date(Date.now() - 13 * 24 * 60 * 60 * 1000); const possiblePartners = await FalukantCharacter.findAll({ where: { id: { [Op.ne]: character.id }, createdAt: { [Op.lt]: thirteenDaysAgo }, }, order: [sequelize.fn('RANDOM')], }); if (possiblePartners.length === 0) { return []; } const newProposals = []; for (let i = 0; i < proposalCount; i++) { const partner = possiblePartners[i % possiblePartners.length]; const createdProposal = await MarriageProposal.create({ requesterCharacterId: character.id, proposedCharacterId: partner.id, courtingProgress: 0, }); newProposals.push(createdProposal); } proposals = newProposals; } return proposals; } } export default new FalukantService();