From f92b62e55b0debd890fabfc8ed249da7aaae0d09 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 13 Apr 2026 15:58:30 +0200 Subject: [PATCH] feat(falukant): enhance tax calculation and update messaging parameters - Updated the tax calculation logic in FalukantService to include character-based checks for political offices, improving accuracy in tax assessments. - Enhanced the MessagesDialog component to incorporate additional parameters such as satisfaction, threshold_percent, director_id, and director_character_id, providing more detailed notifications. - Added new localization entries for director resignation risk notifications in multiple languages, ensuring users receive clear and contextual information regarding director dynamics. --- ...hip-state-marriage-satisfaction-column.cjs | 54 ++++++++ backend/services/falukantService.js | 9 +- .../sql/fix_schema_drift_falukant_compat.sql | 126 ++++++++++++++++++ .../components/falukant/MessagesDialog.vue | 12 ++ frontend/src/i18n/locales/ceb/falukant.json | 3 + frontend/src/i18n/locales/de/falukant.json | 3 + frontend/src/i18n/locales/en/falukant.json | 3 + frontend/src/i18n/locales/es/falukant.json | 3 + frontend/src/i18n/locales/fr/falukant.json | 3 + 9 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/20260413190000-add-relationship-state-marriage-satisfaction-column.cjs create mode 100644 backend/sql/fix_schema_drift_falukant_compat.sql diff --git a/backend/migrations/20260413190000-add-relationship-state-marriage-satisfaction-column.cjs b/backend/migrations/20260413190000-add-relationship-state-marriage-satisfaction-column.cjs new file mode 100644 index 0000000..bf65cad --- /dev/null +++ b/backend/migrations/20260413190000-add-relationship-state-marriage-satisfaction-column.cjs @@ -0,0 +1,54 @@ +'use strict'; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data.relationship_state + ADD COLUMN IF NOT EXISTS marriage_satisfaction integer; + `); + + await queryInterface.sequelize.query(` + UPDATE falukant_data.relationship_state + SET marriage_satisfaction = 55 + WHERE marriage_satisfaction IS NULL; + `); + + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data.relationship_state + ALTER COLUMN marriage_satisfaction SET DEFAULT 55; + `); + + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data.relationship_state + ALTER COLUMN marriage_satisfaction SET NOT NULL; + `); + + await queryInterface.sequelize.query(` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'relationship_state_marriage_satisfaction_check' + AND connamespace = 'falukant_data'::regnamespace + ) THEN + ALTER TABLE falukant_data.relationship_state + ADD CONSTRAINT relationship_state_marriage_satisfaction_check + CHECK (marriage_satisfaction >= 0 AND marriage_satisfaction <= 100); + END IF; + END $$; + `); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data.relationship_state + DROP CONSTRAINT IF EXISTS relationship_state_marriage_satisfaction_check; + `); + + await queryInterface.sequelize.query(` + ALTER TABLE falukant_data.relationship_state + DROP COLUMN IF EXISTS marriage_satisfaction; + `); + } +}; diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index e50063a..2751f6c 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -326,9 +326,16 @@ async function calcRegionalSellPrice(product, knowledgeFactor, regionId, worthPe async function getCumulativeTaxPercentWithExemptions(userId, regionId) { if (!regionId) return 0; if (await hasTitleTaxExempt(userId)) return 0; + const character = await FalukantCharacter.findOne({ + where: { userId }, + attributes: ['id'] + }); + if (!character?.id) { + return await getCumulativeTaxPercent(regionId); + } // fetch user's political offices (active) and their region types const offices = await PoliticalOffice.findAll({ - where: { userId }, + where: { characterId: character.id }, include: [{ model: PoliticalOfficeType, as: 'type', attributes: ['name'] }, { model: RegionData, as: 'region', include: [{ model: RegionType, as: 'regionType', attributes: ['labelTr'] }] }] }); diff --git a/backend/sql/fix_schema_drift_falukant_compat.sql b/backend/sql/fix_schema_drift_falukant_compat.sql new file mode 100644 index 0000000..1139aab --- /dev/null +++ b/backend/sql/fix_schema_drift_falukant_compat.sql @@ -0,0 +1,126 @@ +-- Falukant Schema-Drift Hotfix (idempotent) +-- Ziel: Kompatibilitaet zwischen aktuellem App-Schema und Legacy/Daemon-Queries. +-- +-- Behebt: +-- 1) falukant_data.character.highest_church_hierarchy_ever +-- 2) falukant_predefine.firstname.label / lastname.label +-- 3) falukant_predefine.political_office_benefit.political_office_type_id +-- 4) falukant_type.title.tr (Alias zu label_tr) +-- +-- Ausfuehrung: +-- psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f backend/sql/fix_schema_drift_falukant_compat.sql + +BEGIN; + +-- 1) character.highest_church_hierarchy_ever +ALTER TABLE IF EXISTS falukant_data."character" + ADD COLUMN IF NOT EXISTS highest_church_hierarchy_ever INTEGER NOT NULL DEFAULT 0; + +-- 2) firstname/lastname: label <-> name kompatibel halten +ALTER TABLE IF EXISTS falukant_predefine.firstname + ADD COLUMN IF NOT EXISTS label TEXT; + +ALTER TABLE IF EXISTS falukant_predefine.lastname + ADD COLUMN IF NOT EXISTS label TEXT; + +UPDATE falukant_predefine.firstname +SET label = COALESCE(label, name), + name = COALESCE(name, label) +WHERE label IS NULL OR name IS NULL; + +UPDATE falukant_predefine.lastname +SET label = COALESCE(label, name), + name = COALESCE(name, label) +WHERE label IS NULL OR name IS NULL; + +-- 3) political_office_benefit: political_office_type_id <-> office_type_id +ALTER TABLE IF EXISTS falukant_predefine.political_office_benefit + ADD COLUMN IF NOT EXISTS political_office_type_id INTEGER; + +UPDATE falukant_predefine.political_office_benefit +SET political_office_type_id = COALESCE(political_office_type_id, office_type_id), + office_type_id = COALESCE(office_type_id, political_office_type_id) +WHERE political_office_type_id IS NULL OR office_type_id IS NULL; + +-- FKs fuer beide Spalten absichern (nur falls fehlend) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'political_office_benefit_office_type_id_fkey' + AND connamespace = 'falukant_predefine'::regnamespace + ) THEN + ALTER TABLE falukant_predefine.political_office_benefit + ADD CONSTRAINT political_office_benefit_office_type_id_fkey + FOREIGN KEY (office_type_id) + REFERENCES falukant_type.political_office_type(id) + ON DELETE CASCADE; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'political_office_benefit_political_office_type_id_fkey' + AND connamespace = 'falukant_predefine'::regnamespace + ) THEN + ALTER TABLE falukant_predefine.political_office_benefit + ADD CONSTRAINT political_office_benefit_political_office_type_id_fkey + FOREIGN KEY (political_office_type_id) + REFERENCES falukant_type.political_office_type(id) + ON DELETE CASCADE; + END IF; +END $$; + +CREATE OR REPLACE FUNCTION falukant_predefine.sync_political_office_benefit_type_ids() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.office_type_id IS NULL THEN + NEW.office_type_id := NEW.political_office_type_id; + END IF; + IF NEW.political_office_type_id IS NULL THEN + NEW.political_office_type_id := NEW.office_type_id; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_sync_political_office_benefit_type_ids + ON falukant_predefine.political_office_benefit; + +CREATE TRIGGER trg_sync_political_office_benefit_type_ids +BEFORE INSERT OR UPDATE ON falukant_predefine.political_office_benefit +FOR EACH ROW +EXECUTE FUNCTION falukant_predefine.sync_political_office_benefit_type_ids(); + +-- 4) title.tr <-> label_tr +ALTER TABLE IF EXISTS falukant_type.title + ADD COLUMN IF NOT EXISTS tr TEXT; + +UPDATE falukant_type.title +SET tr = COALESCE(tr, label_tr), + label_tr = COALESCE(label_tr, tr) +WHERE tr IS NULL OR label_tr IS NULL; + +CREATE OR REPLACE FUNCTION falukant_type.sync_title_tr_label() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.tr IS NULL THEN + NEW.tr := NEW.label_tr; + END IF; + IF NEW.label_tr IS NULL THEN + NEW.label_tr := NEW.tr; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_sync_title_tr_label + ON falukant_type.title; + +CREATE TRIGGER trg_sync_title_tr_label +BEFORE INSERT OR UPDATE ON falukant_type.title +FOR EACH ROW +EXECUTE FUNCTION falukant_type.sync_title_tr_label(); + +COMMIT; diff --git a/frontend/src/components/falukant/MessagesDialog.vue b/frontend/src/components/falukant/MessagesDialog.vue index 72bc079..8763f9c 100644 --- a/frontend/src/components/falukant/MessagesDialog.vue +++ b/frontend/src/components/falukant/MessagesDialog.vue @@ -364,6 +364,18 @@ export default { if (base.risk_percent !== undefined) { params.risk_percent = base.risk_percent; } + if (base.satisfaction !== undefined) { + params.satisfaction = base.satisfaction; + } + if (base.threshold_percent !== undefined) { + params.threshold_percent = base.threshold_percent; + } + if (base.director_id !== undefined) { + params.director_id = base.director_id; + } + if (base.director_character_id !== undefined) { + params.director_character_id = base.director_character_id; + } if (base.affection !== undefined) { params.affection = base.affection; } diff --git a/frontend/src/i18n/locales/ceb/falukant.json b/frontend/src/i18n/locales/ceb/falukant.json index 45d50b3..56ae802 100644 --- a/frontend/src/i18n/locales/ceb/falukant.json +++ b/frontend/src/i18n/locales/ceb/falukant.json @@ -34,6 +34,9 @@ "notifications": { "notify_election_created": "Giskedyul ang usa ka bag-ong eleksiyon.", "notify_office_filled": "Na puno ang usa ka politikal nga opisina.", + "director": { + "resignation_risk_high": "Taas ang risgo nga mubiyaa ang direktor: risgo {risk_percent}% (threshold {threshold_percent}%). Karon nga satisfaction {satisfaction}." + }, "director_death": "Namatay si {characterName} sa edad nga {ageYears}. Isip amo, kinahanglan kang magtudlo og bag-ong direktor.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "Namatay si {characterName} sa edad nga {ageYears}.{regionLabel}{spouses}{children}{lovers}", "child_death": "Namatay ang imong anak nga si {characterName} sa edad nga {ageYears}.{regionLabel}", diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index 7ea1e76..28b2497 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -48,6 +48,9 @@ "notifications": { "notify_election_created": "Es wurde eine neue Wahl ausgeschrieben.", "notify_office_filled": "Ein politisches Amt wurde neu besetzt.", + "director": { + "resignation_risk_high": "Hohe Kündigungsgefahr bei einem Direktor: Risiko {risk_percent}% (Schwelle {threshold_percent}%). Zufriedenheit aktuell {satisfaction}." + }, "director_death": "{characterName} ist im Alter von {ageYears} Jahren verstorben. Als Arbeitgeber musst du die Direktion neu besetzen.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} ist im Alter von {ageYears} Jahren verstorben.{regionLabel}{spouses}{children}{lovers}", "child_death": "Dein Kind {characterName} ist im Alter von {ageYears} Jahren verstorben.{regionLabel}", diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index 8f95a51..d1ad4e7 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -34,6 +34,9 @@ "notifications": { "notify_election_created": "A new election has been scheduled.", "notify_office_filled": "A political office has been filled.", + "director": { + "resignation_risk_high": "High resignation risk for a director: risk {risk_percent}% (threshold {threshold_percent}%). Current satisfaction {satisfaction}." + }, "director_death": "{characterName} died at the age of {ageYears}. As employer you need to appoint a new director.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} died at the age of {ageYears}.{regionLabel}{spouses}{children}{lovers}", "child_death": "Your child {characterName} died at the age of {ageYears}.{regionLabel}", diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index 6c981ab..ef5315b 100644 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -48,6 +48,9 @@ "notifications": { "notify_election_created": "Se ha convocado una nueva elección.", "notify_office_filled": "Se ha cubierto un cargo político.", + "director": { + "resignation_risk_high": "Alto riesgo de renuncia de un director: riesgo {risk_percent}% (umbral {threshold_percent}%). Satisfacción actual {satisfaction}." + }, "director_death": "{characterName} ha fallecido a la edad de {ageYears} años. Como empleador debes nombrar un nuevo director.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} ha fallecido a la edad de {ageYears} años.{regionLabel}{spouses}{children}{lovers}", "child_death": "Tu hijo/a {characterName} ha fallecido a la edad de {ageYears} años.{regionLabel}", diff --git a/frontend/src/i18n/locales/fr/falukant.json b/frontend/src/i18n/locales/fr/falukant.json index 16b0cc2..1aa9115 100644 --- a/frontend/src/i18n/locales/fr/falukant.json +++ b/frontend/src/i18n/locales/fr/falukant.json @@ -48,6 +48,9 @@ "notifications": { "notify_election_created": "Une nouvelle élection a été déclenchée.", "notify_office_filled": "Une fonction politique a été pourvue.", + "director": { + "resignation_risk_high": "Risque élevé de démission d’un directeur : risque {risk_percent}% (seuil {threshold_percent}%). Satisfaction actuelle {satisfaction}." + }, "director_death": "{characterName} est décédé à l'âge de {ageYears}. En tant qu'employeur, vous devez remplir le conseil d'administration.{regionLabel}{spouses}{children}{lovers}", "relationship_death": "{characterName} est décédé à l'âge de {ageYears}.{regionLabel}{spouses}{children}{lovers}", "child_death": "Votre enfant {characterName} est décédé à l'âge de {ageYears}.{regionLabel}",