Files
yourpart3/SELL_OVERVIEW.md

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:

  1. Lädt User, Branch, Character, Stock
  2. Lädt Inventory mit ProductType und Knowledge
  3. Berechnet Preis pro Einheit mit calcRegionalSellPrice()
  4. Berechnet kumulative Steuer mit politischen Befreiungen
  5. Passt Preis an (Inflation basierend auf Steuer)
  6. Berechnet Revenue, Tax, Net
  7. Aktualisiert Geld für Verkäufer und Treasury
  8. Entfernt verkaufte Items aus Inventory
  9. Erstellt/aktualisiert DaySell Eintrag
  10. 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 (via updateFalukantUserMoney)

2. sellAllProducts(hashedUserId, branchId)

Verkauft alle Produkte eines Branches.

Ablauf:

  1. Lädt User, Branch mit Stocks
  2. Lädt alle Inventory Items mit ProductType, Knowledge, Stock, Branch
  3. Für jedes Item:
    • Berechnet Preis pro Einheit
    • Berechnet kumulative Steuer
    • Passt Preis an
    • Erstellt/aktualisiert DaySell Eintrag
  4. Berechnet Gesamt-Tax pro Region
  5. Aktualisiert Geld für Verkäufer und Treasury
  6. Löscht alle Inventory Items
  7. 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 (via updateFalukantUserMoney)

3. addSellItem(branchId, userId, productId, quantity)

Erstellt oder aktualisiert einen DaySell Eintrag für einen Verkauf.

Ablauf:

  1. Lädt Branch
  2. Sucht nach existierendem DaySell Eintrag
  3. 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:

  1. Lädt Character des Users
  2. Lädt alle PoliticalOffices des Characters
  3. Bestimmt befreite Region-Typen basierend auf Ämtern
  4. 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:

  1. Basispreis = product.sellCost * (worthPercent / 100)
  2. Min = basePrice * 0.6
  3. Max = basePrice
  4. Preis = min + (max - min) * (knowledgeFactor / 100)

Steueranpassung:

  1. inflationFactor = cumulativeTax >= 100 ? 1 : (1 / (1 - cumulativeTax / 100))
  2. adjustedPricePerUnit = pricePerUnit * inflationFactor
  3. revenue = quantity * adjustedPricePerUnit
  4. taxValue = revenue * cumulativeTax / 100
  5. net = 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

  1. Inventory wird nach Verkauf gelöscht/aktualisiert: Items werden aus der Inventory entfernt oder die Menge reduziert.

  2. DaySell wird aggregiert: Wenn bereits ein DaySell Eintrag für Region/Product/Seller existiert, wird die Menge addiert.

  3. Steuer wird an Treasury gebucht: Wenn TREASURY_FALUKANT_USER_ID gesetzt ist, wird die Steuer an diesen User gebucht.

  4. Socket-Notifications: Nach jedem Verkauf werden falukantUpdateStatus und falukantBranchUpdate Events gesendet.

  5. Politische Befreiungen: Bestimmte politische Ämter befreien von Steuern in bestimmten Region-Typen. Chancellor befreit von allen Steuern.

  6. 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 zu falukant_data.stock)
  • product_id (FK zu falukant_type.product)
  • quantity (INTEGER)
  • quality (INTEGER)
  • produced_at (DATE)

falukant_log.sell (DaySell)

  • id (PK)
  • region_id (FK zu falukant_data.region)
  • product_id (FK zu falukant_type.product)
  • seller_id (FK zu falukant_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 zu falukant_type.product)
  • region_id (FK zu falukant_data.region)
  • worth_percent (INTEGER, 0-100)

falukant_data.knowledge

  • id (PK)
  • product_id (FK zu falukant_type.product)
  • character_id (FK zu falukant_data.character)
  • knowledge (INTEGER, 0-99)

falukant_data.political_office

  • id (PK)
  • office_type_id (FK zu falukant_type.political_office_type)
  • character_id (FK zu falukant_data.character)
  • region_id (FK zu falukant_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 zu falukant_type.region)
  • parent_id (FK zu falukant_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 zu falukant_type.region, nullable)

falukant_data.falukant_user

  • id (PK)
  • user_id (FK zu community.user)
  • money (DECIMAL)
  • credit_amount, today_credit_taken, credit_interest_rate
  • certificate
  • main_branch_region_id
  • last_nobility_advance_at
  • created_at, updated_at

falukant_data.character

  • id (PK)
  • user_id (FK zu falukant_data.falukant_user)
  • region_id (FK zu falukant_data.region)
  • first_name, last_name
  • birthdate, gender, health
  • title_of_nobility (FK zu falukant_type.title_of_nobility)
  • mood_id (FK zu falukant_type.mood)
  • created_at, updated_at

falukant_data.branch

  • id (PK)
  • branch_type_id (FK zu falukant_type.branch)
  • region_id (FK zu falukant_data.region)
  • falukant_user_id (FK zu falukant_data.falukant_user)

falukant_data.stock

  • id (PK)
  • branch_id (FK zu falukant_data.branch)
  • stock_type_id (FK zu falukant_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.js
    • backend/models/falukant/log/daysell.js
    • backend/models/falukant/data/town_product_worth.js
    • backend/models/falukant/data/product_knowledge.js
    • backend/models/falukant/data/political_office.js
    • backend/models/falukant/type/political_office_type.js
    • backend/models/falukant/data/region.js
    • backend/models/falukant/type/region.js
    • backend/models/falukant/data/character.js
    • backend/models/falukant/data/user.js
    • backend/models/falukant/data/branch.js
    • backend/models/falukant/data/stock.js
    • backend/models/falukant/type/product.js