20 KiB
Übersicht: Sell-Funktionen und verwendete Models/Tabellen
Sell-Funktionen in falukantService.js
1. sellProduct(hashedUserId, branchId, productId, quality, quantity)
Verkauft ein einzelnes Produkt mit bestimmter Qualität.
Ablauf:
- Lädt User, Branch, Character, Stock
- Lädt Inventory mit ProductType und Knowledge
- Berechnet Preis pro Einheit mit
calcRegionalSellPrice() - Berechnet kumulative Steuer mit politischen Befreiungen
- Passt Preis an (Inflation basierend auf Steuer)
- Berechnet Revenue, Tax, Net
- Aktualisiert Geld für Verkäufer und Treasury
- Entfernt verkaufte Items aus Inventory
- Erstellt/aktualisiert DaySell Eintrag
- Sendet Socket-Notifications
Verwendete Models/Tabellen:
FalukantUser(falukant_data.falukant_user)Branch(falukant_data.branch)FalukantCharacter(falukant_data.character)FalukantStock(falukant_data.stock)Inventory(falukant_data.inventory)ProductType(falukant_type.product)Knowledge(falukant_data.knowledge)TownProductWorth(falukant_data.town_product_worth)RegionData(falukant_data.region)RegionType(falukant_type.region)PoliticalOffice(falukant_data.political_office)PoliticalOfficeType(falukant_type.political_office_type)DaySell(falukant_log.day_sell)MoneyFlow(viaupdateFalukantUserMoney)
2. sellAllProducts(hashedUserId, branchId)
Verkauft alle Produkte eines Branches.
Ablauf:
- Lädt User, Branch mit Stocks
- Lädt alle Inventory Items mit ProductType, Knowledge, Stock, Branch
- Für jedes Item:
- Berechnet Preis pro Einheit
- Berechnet kumulative Steuer
- Passt Preis an
- Erstellt/aktualisiert DaySell Eintrag
- Berechnet Gesamt-Tax pro Region
- Aktualisiert Geld für Verkäufer und Treasury
- Löscht alle Inventory Items
- Sendet Socket-Notifications
Verwendete Models/Tabellen:
FalukantUser(falukant_data.falukant_user)Branch(falukant_data.branch)FalukantStock(falukant_data.stock)FalukantStockType(falukant_type.stock)FalukantCharacter(falukant_data.character)Inventory(falukant_data.inventory)ProductType(falukant_type.product)Knowledge(falukant_data.knowledge)TownProductWorth(falukant_data.town_product_worth)RegionData(falukant_data.region)RegionType(falukant_type.region)PoliticalOffice(falukant_data.political_office)PoliticalOfficeType(falukant_type.political_office_type)DaySell(falukant_log.day_sell)MoneyFlow(viaupdateFalukantUserMoney)
3. addSellItem(branchId, userId, productId, quantity)
Erstellt oder aktualisiert einen DaySell Eintrag für einen Verkauf.
Ablauf:
- Lädt Branch
- Sucht nach existierendem DaySell Eintrag
- Erstellt neuen oder aktualisiert existierenden Eintrag
Verwendete Models/Tabellen:
Branch(falukant_data.branch)DaySell(falukant_log.day_sell)
Hilfsfunktionen
calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null)
Berechnet den Verkaufspreis eines Produkts basierend auf:
- Basispreis (
product.sellCost) - Regionalem Worth-Percent (
town_product_worth.worth_percent) - Knowledge-Faktor (0-100)
Verwendete Models/Tabellen:
ProductType(falukant_type.product)TownProductWorth(falukant_data.town_product_worth)
getCumulativeTaxPercent(regionId)
Berechnet die kumulative Steuer für eine Region und alle Vorfahren (rekursiv).
SQL Query:
WITH RECURSIVE ancestors AS (
SELECT id, parent_id, tax_percent
FROM falukant_data.region r
WHERE id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent
FROM falukant_data.region reg
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;
Verwendete Tabellen:
falukant_data.region
getCumulativeTaxPercentWithExemptions(userId, regionId)
Berechnet die kumulative Steuer mit politischen Befreiungen.
Ablauf:
- Lädt Character des Users
- Lädt alle PoliticalOffices des Characters
- Bestimmt befreite Region-Typen basierend auf Ämtern
- Berechnet kumulative Steuer, aber schließt befreite Region-Typen aus
SQL Query:
WITH RECURSIVE ancestors AS (
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
FROM falukant_data.region r
JOIN falukant_type.region rt ON rt.id = r.region_type_id
WHERE r.id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
FROM falukant_data.region reg
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(CASE WHEN ARRAY[...] && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;
Verwendete Models/Tabellen:
FalukantCharacter(falukant_data.character)PoliticalOffice(falukant_data.political_office)PoliticalOfficeType(falukant_type.political_office_type)RegionData(falukant_data.region)RegionType(falukant_type.region)
Politische Steuerbefreiungen:
const POLITICAL_TAX_EXEMPTIONS = {
'council': ['city'],
'taxman': ['city', 'county'],
'treasurerer': ['city', 'county', 'shire'],
'super-state-administrator': ['city', 'county', 'shire', 'markgrave', 'duchy'],
'chancellor': ['city','county','shire','markgrave','duchy'] // = alle Typen
};
Model-Definitionen
Inventory (falukant_data.inventory)
// backend/models/falukant/data/inventory.js
- id
- stockId (FK zu falukant_data.stock)
- productId (FK zu falukant_type.product)
- quantity
- quality
- producedAt
DaySell (falukant_log.day_sell)
// backend/models/falukant/log/daysell.js
- id
- regionId (FK zu falukant_data.region)
- productId (FK zu falukant_type.product)
- sellerId (FK zu falukant_data.falukant_user)
- quantity
- createdAt
- updatedAt
TownProductWorth (falukant_data.town_product_worth)
// backend/models/falukant/data/town_product_worth.js
- id
- productId (FK zu falukant_type.product)
- regionId (FK zu falukant_data.region)
- worthPercent (0-100)
Knowledge (falukant_data.knowledge)
// backend/models/falukant/data/product_knowledge.js
- id
- productId (FK zu falukant_type.product)
- characterId (FK zu falukant_data.character)
- knowledge (0-99)
Wichtige SQL-Queries
1. Inventory mit ProductType und Knowledge laden
Inventory.findAll({
where: { quality },
include: [
{
model: ProductType,
as: 'productType',
required: true,
where: { id: productId },
include: [
{
model: Knowledge,
as: 'knowledges',
required: false,
where: { characterId: character.id }
}
]
}
]
})
2. Kumulative Steuer mit Befreiungen berechnen
Siehe getCumulativeTaxPercentWithExemptions() oben.
Preisberechnung
Formel für calcRegionalSellPrice:
- Basispreis =
product.sellCost * (worthPercent / 100) - Min =
basePrice * 0.6 - Max =
basePrice - Preis =
min + (max - min) * (knowledgeFactor / 100)
Steueranpassung:
inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100))adjustedPricePerUnit = pricePerUnit * inflationFactorrevenue = quantity * adjustedPricePerUnittaxValue = revenue * cumulativeTax / 100net = revenue - taxValue
Vollständige Code-Snippets
calcRegionalSellPrice()
async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPercent = null) {
if (worthPercent === null) {
const townWorth = await TownProductWorth.findOne({
where: { productId: product.id, regionId: regionId }
});
worthPercent = townWorth?.worthPercent || 50; // Default 50% wenn nicht gefunden
}
// Basispreis basierend auf regionalem worthPercent
const basePrice = product.sellCost * (worthPercent / 100);
// Dann Knowledge-Faktor anwenden
const min = basePrice * 0.6;
const max = basePrice;
return min + (max - min) * (knowledgeFactor / 100);
}
getCumulativeTaxPercent()
async function getCumulativeTaxPercent(regionId) {
if (!regionId) return 0;
const rows = await sequelize.query(
`WITH RECURSIVE ancestors AS (
SELECT id, parent_id, tax_percent
FROM falukant_data.region r
WHERE id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent
FROM falukant_data.region reg
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(tax_percent),0) AS total FROM ancestors;`,
{
replacements: { id: regionId },
type: sequelize.QueryTypes.SELECT
}
);
const val = rows?.[0]?.total ?? 0;
return parseFloat(val) || 0;
}
getCumulativeTaxPercentWithExemptions() (vereinfacht)
async function getCumulativeTaxPercentWithExemptions(userId, regionId) {
if (!regionId) return 0;
// Character finden
const character = await FalukantCharacter.findOne({
where: { userId },
attributes: ['id']
});
if (!character) return 0;
// Politische Ämter laden
const offices = await PoliticalOffice.findAll({
where: { characterId: character.id },
include: [
{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] },
{
model: RegionData,
as: 'region',
include: [{
model: RegionType,
as: 'regionType',
attributes: ['labelTr']
}]
}
]
});
// Befreite Region-Typen bestimmen
const exemptTypes = new Set();
let hasChancellor = false;
for (const o of offices) {
const name = o.type?.name;
if (!name) continue;
if (name === 'chancellor') { hasChancellor = true; break; }
const allowed = POLITICAL_TAX_EXEMPTIONS[name];
if (allowed && Array.isArray(allowed)) {
for (const t of allowed) exemptTypes.add(t);
}
}
if (hasChancellor) return 0;
// SQL Query mit Befreiungen
const exemptTypesArray = Array.from(exemptTypes);
const exemptTypesString = exemptTypesArray.length > 0
? `ARRAY[${exemptTypesArray.map(t => `'${t.replace(/'/g, "''")}'`).join(',')}]`
: `ARRAY[]::text[]`;
const rows = await sequelize.query(
`WITH RECURSIVE ancestors AS (
SELECT r.id, r.parent_id, r.tax_percent, rt.label_tr as region_type
FROM falukant_data.region r
JOIN falukant_type.region rt ON rt.id = r.region_type_id
WHERE r.id = :id
UNION ALL
SELECT reg.id, reg.parent_id, reg.tax_percent, rt2.label_tr
FROM falukant_data.region reg
JOIN falukant_type.region rt2 ON rt2.id = reg.region_type_id
JOIN ancestors a ON reg.id = a.parent_id
)
SELECT COALESCE(SUM(CASE WHEN ${exemptTypesString} && ARRAY[region_type]::text[] THEN 0 ELSE tax_percent END),0) AS total FROM ancestors;`,
{
replacements: { id: regionId },
type: sequelize.QueryTypes.SELECT
}
);
const val = rows?.[0]?.total ?? 0;
return parseFloat(val) || 0;
}
sellProduct() (Kern-Logik)
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');
// Inventory laden
const inventory = await Inventory.findAll({
where: { 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 pricePerUnit = await calcRegionalSellPrice(item, knowledgeVal, branch.regionId);
// Steuer berechnen
const cumulativeTax = await getCumulativeTaxPercentWithExemptions(user.id, branch.regionId);
const inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100));
const adjustedPricePerUnit = Math.round(pricePerUnit * inflationFactor * 100) / 100;
const revenue = quantity * adjustedPricePerUnit;
// Tax und Net berechnen
const taxValue = Math.round((revenue * cumulativeTax / 100) * 100) / 100;
const net = Math.round((revenue - taxValue) * 100) / 100;
// Geld aktualisieren
const moneyResult = await updateFalukantUserMoney(user.id, net, `Product sale (net)`, user.id);
if (!moneyResult.success) throw new Error('Failed to update money for seller');
// Steuer an Treasury buchen
const treasuryId = process.env.TREASURY_FALUKANT_USER_ID;
if (treasuryId && taxValue > 0) {
const taxResult = await updateFalukantUserMoney(parseInt(treasuryId, 10), taxValue, `Sales tax (${cumulativeTax}%)`, user.id);
if (!taxResult.success) throw new Error('Failed to update money for treasury');
}
// Inventory aktualisieren
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;
}
}
// DaySell Eintrag erstellen/aktualisieren
await this.addSellItem(branchId, user.id, productId, quantity);
// Notifications senden
notifyUser(user.user.hashedId, 'falukantUpdateStatus', {});
notifyUser(user.user.hashedId, 'falukantBranchUpdate', { branchId: branch.id });
return { success: true };
}
addSellItem()
async addSellItem(branchId, userId, productId, quantity) {
const branch = await Branch.findOne({
where: { id: branchId },
});
const daySell = await DaySell.findOne({
where: {
regionId: branch.regionId,
productId: productId,
sellerId: userId,
}
});
if (daySell) {
daySell.quantity += quantity;
await daySell.save();
} else {
await DaySell.create({
regionId: branch.regionId,
productId: productId,
sellerId: userId,
quantity: quantity,
});
}
}
Wichtige Hinweise
-
Inventory wird nach Verkauf gelöscht/aktualisiert: Items werden aus der Inventory entfernt oder die Menge reduziert.
-
DaySell wird aggregiert: Wenn bereits ein DaySell Eintrag für Region/Product/Seller existiert, wird die Menge addiert.
-
Steuer wird an Treasury gebucht: Wenn
TREASURY_FALUKANT_USER_IDgesetzt ist, wird die Steuer an diesen User gebucht. -
Socket-Notifications: Nach jedem Verkauf werden
falukantUpdateStatusundfalukantBranchUpdateEvents gesendet. -
Politische Befreiungen: Bestimmte politische Ämter befreien von Steuern in bestimmten Region-Typen. Chancellor befreit von allen Steuern.
-
Preis-Inflation: Der Preis wird basierend auf der Steuer inflatiert, damit der Netto-Betrag für den Verkäufer gleich bleibt.
Tabellenübersicht
falukant_data.inventory
id(PK)stock_id(FK zufalukant_data.stock)product_id(FK zufalukant_type.product)quantity(INTEGER)quality(INTEGER)produced_at(DATE)
falukant_log.sell (DaySell)
id(PK)region_id(FK zufalukant_data.region)product_id(FK zufalukant_type.product)seller_id(FK zufalukant_data.falukant_user)quantity(INTEGER)sell_timestamp(DATE)- Unique Index:
(seller_id, product_id, region_id)
falukant_data.town_product_worth
id(PK)product_id(FK zufalukant_type.product)region_id(FK zufalukant_data.region)worth_percent(INTEGER, 0-100)
falukant_data.knowledge
id(PK)product_id(FK zufalukant_type.product)character_id(FK zufalukant_data.character)knowledge(INTEGER, 0-99)
falukant_data.political_office
id(PK)office_type_id(FK zufalukant_type.political_office_type)character_id(FK zufalukant_data.character)region_id(FK zufalukant_data.region)created_at,updated_at
falukant_type.political_office_type
id(PK)name(STRING) - z.B. 'council', 'taxman', 'treasurerer', 'super-state-administrator', 'chancellor'seats_per_region(INTEGER)region_type(STRING)term_length(INTEGER)
falukant_data.region
id(PK)name(STRING)region_type_id(FK zufalukant_type.region)parent_id(FK zufalukant_data.region, nullable)map(JSONB)tax_percent(DECIMAL)
falukant_type.region
id(PK)label_tr(STRING) - z.B. 'city', 'county', 'shire', 'markgrave', 'duchy'parent_id(FK zufalukant_type.region, nullable)
falukant_data.falukant_user
id(PK)user_id(FK zucommunity.user)money(DECIMAL)credit_amount,today_credit_taken,credit_interest_ratecertificatemain_branch_region_idlast_nobility_advance_atcreated_at,updated_at
falukant_data.character
id(PK)user_id(FK zufalukant_data.falukant_user)region_id(FK zufalukant_data.region)first_name,last_namebirthdate,gender,healthtitle_of_nobility(FK zufalukant_type.title_of_nobility)mood_id(FK zufalukant_type.mood)created_at,updated_at
falukant_data.branch
id(PK)branch_type_id(FK zufalukant_type.branch)region_id(FK zufalukant_data.region)falukant_user_id(FK zufalukant_data.falukant_user)
falukant_data.stock
id(PK)branch_id(FK zufalukant_data.branch)stock_type_id(FK zufalukant_type.stock)quantity(INTEGER)product_quality(INTEGER, nullable)
falukant_type.product
id(PK)label_tr(STRING, unique)category(INTEGER)production_time(INTEGER)sell_cost(INTEGER)
Dateipfade
- Service:
backend/services/falukantService.js - Models:
backend/models/falukant/data/inventory.jsbackend/models/falukant/log/daysell.jsbackend/models/falukant/data/town_product_worth.jsbackend/models/falukant/data/product_knowledge.jsbackend/models/falukant/data/political_office.jsbackend/models/falukant/type/political_office_type.jsbackend/models/falukant/data/region.jsbackend/models/falukant/type/region.jsbackend/models/falukant/data/character.jsbackend/models/falukant/data/user.jsbackend/models/falukant/data/branch.jsbackend/models/falukant/data/stock.jsbackend/models/falukant/type/product.js