// Centralized SQL strings for workers. pub const QUERY_UPDATE_MONEY: &str = r#" SELECT falukant_data.update_money($1, $2, $3); "#; /// Insert in money_history für UI/Verlauf; Betrag als Parameter für zuverlässige Speicherung. pub const QUERY_INSERT_MONEY_HISTORY: &str = r#" INSERT INTO falukant_log.money_history (user_id, change, action, created_at) VALUES ($1, $2::numeric, $3, NOW()) "#; pub const QUERY_GET_MONEY: &str = r#" SELECT money FROM falukant_data.falukant_user WHERE id = $1; "#; pub const QUERY_GET_RANDOM_USER: &str = r#" SELECT id FROM falukant_data.falukant_user ORDER BY RANDOM() LIMIT 1; "#; /// Nur NPC-Kleinkinder (user_id IS NULL); Spieler-Charaktere sind von plötzlichem Kindstod ausgenommen. pub const QUERY_GET_RANDOM_INFANT: &str = r#" SELECT c.id AS character_id, c.user_id, CURRENT_DATE - c.birthdate::date AS age_days FROM falukant_data."character" c WHERE c.user_id IS NULL AND c.health > 0 AND CURRENT_DATE - c.birthdate::date <= 730 ORDER BY RANDOM() LIMIT 1; "#; pub const QUERY_GET_RANDOM_CITY: &str = r#" SELECT r.id AS region_id FROM falukant_data.region r JOIN falukant_type.region tr ON r.region_type_id = tr.id WHERE tr.label_tr = 'city' ORDER BY RANDOM() LIMIT 1; "#; pub const QUERY_GET_AFFECTED_USERS: &str = r#" SELECT DISTINCT b.falukant_user_id AS user_id FROM falukant_data.branch b WHERE b.region_id = $1 AND b.falukant_user_id IS NOT NULL; "#; pub const QUERY_UPDATE_WEATHER: &str = r#" WITH all_regions AS ( SELECT DISTINCT r.id AS region_id FROM falukant_data.region r JOIN falukant_type.region tr ON r.region_type_id = tr.id WHERE tr.label_tr = 'city' ) INSERT INTO falukant_data.weather (region_id, weather_type_id) SELECT ar.region_id, (SELECT wt.id FROM falukant_type.weather wt ORDER BY random() + ar.region_id * 0 LIMIT 1) FROM all_regions ar ON CONFLICT (region_id) DO UPDATE SET weather_type_id = EXCLUDED.weather_type_id; "#; pub const QUERY_INSERT_NOTIFICATION: &str = r#" INSERT INTO falukant_log.notification (user_id, tr, shown, created_at, updated_at) VALUES ($1, $2, FALSE, NOW(), NOW()); "#; // Product pricing (nur sell_cost; für Produktions-Stückkosten siehe QUERY_GET_PRODUCT_CATEGORY_AND_USER_CERTIFICATE) #[allow(dead_code)] pub const QUERY_GET_PRODUCT_COST: &str = r#" SELECT sell_cost FROM falukant_type.product WHERE id = $1; "#; /// Produktklasse + Spieler-Zertifikat für Stückkosten (kein „teurer wegen höherem Zertifikat“). pub const QUERY_GET_PRODUCT_CATEGORY_AND_USER_CERTIFICATE: &str = r#" SELECT COALESCE(p.category, 1)::int AS category, COALESCE(u.certificate, 1)::int AS certificate FROM falukant_type.product p CROSS JOIN falukant_data.falukant_user u WHERE p.id = $1::int AND u.id = $2::int; "#; pub const QUERY_GET_DIRECTORS: &str = r#" SELECT d.may_produce, d.may_sell, d.may_start_transport, b.id AS branch_id, fu.id AS falukantUserId, d.id FROM falukant_data.director d JOIN falukant_data.falukant_user fu ON fu.id = d.employer_user_id JOIN falukant_data.character c ON c.id = d.director_character_id JOIN falukant_data.branch b ON b.region_id = c.region_id AND b.falukant_user_id = fu.id WHERE current_time BETWEEN '08:00:00' AND '17:00:00'; "#; pub const QUERY_GET_BEST_PRODUCTION: &str = r#" SELECT fdu.id falukant_user_id, CAST(fdu.money AS text) AS money, fdu.certificate, ftp.id product_id, ftp.label_tr, COALESCE(ftp.category, 1)::int AS product_category, fdb.region_id, (SELECT SUM(quantity) FROM falukant_data.stock fds WHERE fds.branch_id = fdb.id) AS stock_size, COALESCE((SELECT SUM(COALESCE(fdi.quantity, 0)) FROM falukant_data.stock fds JOIN falukant_data.inventory fdi ON fdi.stock_id = fds.id WHERE fds.branch_id = fdb.id), 0) AS used_in_stock, -- Ranking = regionaler Erlös pro Zeiteinheit (wie „Erlös/Minute“ in der Oberfläche): -- Listenpreis * regionaler Marktanteil / Produktionsdauer — ohne Wissens-Additiv und ohne alte Kostenstrafe -- im Zähler (die echte Stückkosten berechnet der Director in Rust). ( (ftp.sell_cost * (fdtpw.worth_percent / 100.0)) / NULLIF(ftp.production_time::float8, 0.0) ) AS worth, fdb.id AS branch_id, (SELECT COUNT(id) FROM falukant_data.production WHERE branch_id = fdb.id) AS running_productions, COALESCE((SELECT SUM(COALESCE(fdp.quantity, 0)) quantity FROM falukant_data.production fdp WHERE fdp.branch_id = fdb.id), 0) AS running_productions_quantity FROM falukant_data.director fdd JOIN falukant_data.character fdc ON fdc.id = fdd.director_character_id JOIN falukant_data.falukant_user fdu ON fdd.employer_user_id = fdu.id JOIN falukant_data.character user_character ON user_character.user_id = fdu.id JOIN falukant_data.branch fdb ON fdb.falukant_user_id = fdu.id AND fdb.region_id = fdc.region_id JOIN falukant_data.town_product_worth fdtpw ON fdtpw.region_id = fdb.region_id JOIN falukant_data.knowledge fdk_character ON fdk_character.product_id = fdtpw.product_id AND fdk_character.character_id = user_character.id JOIN falukant_data.knowledge fdk_director ON fdk_director.product_id = fdtpw.product_id AND fdk_director.character_id = fdd.director_character_id JOIN falukant_type.product ftp ON ftp.id = fdtpw.product_id AND ftp.category <= fdu.certificate WHERE fdd.id = $1 AND fdb.id = $2 ORDER BY worth DESC LIMIT 1; "#; pub const QUERY_INSERT_PRODUCTION: &str = r#" INSERT INTO falukant_data.production (branch_id, product_id, quantity, weather_type_id) VALUES ($1, $2, $3, (SELECT weather_type_id FROM falukant_data.weather WHERE region_id = $4)); "#; // Character creation related queries (missing from earlier extraction) pub const QUERY_IS_PREVIOUS_DAY_CHARACTER_CREATED: &str = r#" SELECT created_at FROM falukant_data."character" WHERE user_id IS NULL AND created_at::date = CURRENT_DATE ORDER BY created_at DESC LIMIT 1; "#; pub const QUERY_GET_TOWN_REGION_IDS: &str = r#" SELECT fdr.id FROM falukant_data.region fdr JOIN falukant_type.region ftr ON fdr.region_type_id = ftr.id WHERE ftr.label_tr = 'city'; "#; pub const QUERY_LOAD_FIRST_NAMES: &str = r#" SELECT id, gender FROM falukant_predefine.firstname; "#; pub const QUERY_LOAD_LAST_NAMES: &str = r#" SELECT id FROM falukant_predefine.lastname; "#; pub const QUERY_INSERT_CHARACTER: &str = r#" INSERT INTO falukant_data.character( user_id, region_id, first_name, last_name, birthdate, gender, created_at, updated_at, title_of_nobility ) VALUES ( NULL, $1, $2, $3, NOW(), $4, NOW(), NOW(), $5 ); "#; pub const QUERY_GET_ELIGIBLE_NPC_FOR_DEATH: &str = r#" WITH aged AS ( SELECT c.id, (current_date - c.birthdate::date) AS age, c.user_id FROM falukant_data.character c WHERE c.user_id IS NULL AND (current_date - c.birthdate::date) > 60 ), always_sel AS ( SELECT * FROM aged WHERE age > 85 ), random_sel AS ( SELECT * FROM aged WHERE age <= 85 ORDER BY random() LIMIT 10 ) SELECT * FROM always_sel UNION ALL SELECT * FROM random_sel; "#; pub const QUERY_MARK_CHARACTER_DECEASED: &str = r#" DELETE FROM falukant_data.character WHERE id = $1; "#; pub const QUERY_GET_BRANCH_CAPACITY: &str = r#" SELECT (SELECT SUM(quantity) FROM falukant_data.stock fds WHERE fds.branch_id = $1) AS stock_size, COALESCE((SELECT SUM(COALESCE(fdi.quantity, 0)) FROM falukant_data.stock fds JOIN falukant_data.inventory fdi ON fdi.stock_id = fds.id WHERE fds.branch_id = $1), 0) AS used_in_stock, (SELECT COUNT(id) FROM falukant_data.production WHERE branch_id = $1) AS running_productions, COALESCE((SELECT SUM(COALESCE(fdp.quantity, 0)) quantity FROM falukant_data.production fdp WHERE fdp.branch_id = $1), 0) AS running_productions_quantity; "#; pub const QUERY_GET_INVENTORY: &str = r#" SELECT i.id, i.product_id, i.quantity, i.quality, p.sell_cost, fu.id AS user_id, b.region_id, b.id AS branch_id, COALESCE(tpw.worth_percent, 100.0) AS worth_percent FROM falukant_data.inventory i JOIN falukant_data.stock s ON s.id = i.stock_id JOIN falukant_data.branch b ON b.id = s.branch_id JOIN falukant_data.falukant_user fu ON fu.id = b.falukant_user_id JOIN falukant_data.director d ON d.employer_user_id = fu.id JOIN falukant_type.product p ON p.id = i.product_id LEFT JOIN falukant_data.town_product_worth tpw ON tpw.region_id = b.region_id AND tpw.product_id = i.product_id WHERE d.id = $1 AND b.id = $2; "#; pub const QUERY_REMOVE_INVENTORY: &str = r#" DELETE FROM falukant_data.inventory WHERE id = $1; "#; pub const QUERY_ADD_SELL_LOG: &str = r#" INSERT INTO falukant_log.sell (region_id, product_id, quantity, seller_id) VALUES ($1, $2, $3, $4) ON CONFLICT (region_id, product_id, seller_id) DO UPDATE SET quantity = falukant_log.sell.quantity + EXCLUDED.quantity; "#; pub const QUERY_GET_ARRIVED_TRANSPORTS: &str = r#" SELECT t.id, t.product_id, t.size, t.vehicle_id, t.source_region_id, t.target_region_id, b_target.id AS target_branch_id, b_source.id AS source_branch_id, rd.distance AS distance, v.falukant_user_id AS user_id FROM falukant_data.transport AS t JOIN falukant_data.vehicle AS v ON v.id = t.vehicle_id JOIN falukant_type.vehicle AS vt ON vt.id = v.vehicle_type_id JOIN falukant_data.region_distance AS rd ON ((rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id) OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) LEFT JOIN falukant_data.branch AS b_target ON b_target.region_id = t.target_region_id AND b_target.falukant_user_id = v.falukant_user_id LEFT JOIN falukant_data.branch AS b_source ON b_source.region_id = t.source_region_id AND b_source.falukant_user_id = v.falukant_user_id WHERE vt.speed > 0 AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' <= NOW(); "#; pub const QUERY_GET_AVAILABLE_STOCKS: &str = r#" SELECT stock.id, stock.quantity AS total_capacity, ( SELECT COALESCE(SUM(inventory.quantity), 0) FROM falukant_data.inventory WHERE inventory.stock_id = stock.id ) AS filled FROM falukant_data.stock stock JOIN falukant_data.branch branch ON stock.branch_id = branch.id WHERE branch.id = $1 ORDER BY total_capacity DESC; "#; pub const QUERY_INSERT_INVENTORY: &str = r#" INSERT INTO falukant_data.inventory (stock_id, product_id, quantity, quality, produced_at) VALUES ($1, $2, $3, $4, NOW()) RETURNING id; "#; pub const QUERY_UPDATE_INVENTORY_BY_STOCK_PRODUCT: &str = r#" UPDATE falukant_data.inventory SET quantity = quantity + $3, quality = LEAST( 100, ROUND( ((quantity * quality) + ($3 * $4))::numeric / NULLIF(quantity + $3, 0) ) ) WHERE stock_id = $1 AND product_id = $2 RETURNING id; "#; pub const QUERY_UPDATE_VEHICLE_AFTER_TRANSPORT: &str = r#" UPDATE falukant_data.vehicle SET region_id = $2, condition = GREATEST(0, condition - $3::int), available_from = NOW(), updated_at = NOW() WHERE id = $1; "#; pub const QUERY_DELETE_TRANSPORT: &str = r#" DELETE FROM falukant_data.transport WHERE id = $1; "#; #[allow(dead_code)] pub const QUERY_ADD_TRANSPORT_WAITING_NOTIFICATION: &str = r#" INSERT INTO falukant_log.notification (user_id, tr, shown, created_at, updated_at) VALUES ((SELECT c.user_id FROM falukant_data.character c WHERE c.user_id = $1 LIMIT 1), $2, FALSE, NOW(), NOW()); "#; pub const QUERY_UPDATE_TRANSPORT_SIZE: &str = r#" UPDATE falukant_data.transport SET size = $2, updated_at = NOW() WHERE id = $1; "#; // --- Falukant: Transportüberfälle (docs/FALUKANT_TRANSPORT_RAID_DAEMON.md) --- pub const QUERY_RAID_SCHEMA_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'transport' AND column_name = 'guard_count' ) AS ready; "#; pub const QUERY_RAID_ACTIVE_UNDERGROUND: &str = r#" SELECT u.id, u.performer_id, COALESCE(u.parameters::text, '{}') AS parameters FROM falukant_data.underground u JOIN falukant_type.underground t ON t.tr = u.underground_type_id WHERE u.result IS NULL AND t.tr = 'raid_transport' ORDER BY u.created_at ASC LIMIT 100; "#; pub const QUERY_RAID_REGION_ALLOWED: &str = r#" SELECT r.id FROM falukant_data.region r JOIN falukant_type.region rt ON rt.id = r.region_type_id WHERE r.id = $1::int AND rt.id IN (4, 5) AND COALESCE(rt.label_tr, '') <> 'town'; "#; pub const QUERY_RAID_FALUKANT_USER_FOR_CHARACTER: &str = r#" SELECT fu.id AS falukant_user_id, fu.user_id AS app_user_id FROM falukant_data.character c JOIN falukant_data.falukant_user fu ON fu.user_id = c.user_id WHERE c.id = $1::int LIMIT 1; "#; /// Aktive (noch unterwegs) Transporte mit Fracht, Route berührt Region, nicht vom Auftraggeber. pub const QUERY_RAID_CANDIDATE_TRANSPORTS: &str = r#" SELECT t.id AS transport_id, t.size AS transport_size, t.product_id, COALESCE(t.guard_count, 0)::int AS guard_count, v.falukant_user_id AS victim_falukant_user_id, vt.capacity AS vehicle_capacity FROM falukant_data.transport t JOIN falukant_data.vehicle v ON v.id = t.vehicle_id JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id JOIN falukant_data.region_distance rd ON ( (rd.source_region_id = t.source_region_id AND rd.target_region_id = t.target_region_id) OR (rd.source_region_id = t.target_region_id AND rd.target_region_id = t.source_region_id) ) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) WHERE t.product_id IS NOT NULL AND t.size > 0 AND vt.speed > 0 AND t.created_at + (rd.distance / vt.speed::double precision) * INTERVAL '1 minute' > NOW() AND (t.source_region_id = $1::int OR t.target_region_id = $1::int) AND v.falukant_user_id <> $2::int ORDER BY RANDOM() LIMIT 20; "#; /// Bevorzugt eine Niederlassung in der Überfallregion, sonst kleinste branch_id. pub const QUERY_RAID_NEAREST_BRANCH_FOR_USER: &str = r#" SELECT b.id AS branch_id, b.region_id FROM falukant_data.branch b WHERE b.falukant_user_id = $2::int ORDER BY CASE WHEN b.region_id = $1::int THEN 0 ELSE 1 END, b.id ASC LIMIT 1; "#; pub const QUERY_RAID_APP_USER_FOR_FALUKANT: &str = r#" SELECT fu.user_id FROM falukant_data.falukant_user fu WHERE fu.id = $1::int LIMIT 1; "#; pub const QUERY_RAID_UPDATE_UNDERGROUND_RESULT: &str = r#" UPDATE falukant_data.underground SET result = $2::jsonb, updated_at = NOW() WHERE id = $1::int; "#; pub const QUERY_RAID_SUBTRACT_REP_BY_USER: &str = r#" UPDATE falukant_data.character c SET reputation = GREATEST(0::numeric, COALESCE(c.reputation, 50::numeric) - $2::numeric), updated_at = NOW() FROM falukant_data.falukant_user fu WHERE fu.id = $1::int AND fu.user_id = c.user_id AND c.health > 0; "#; pub const QUERY_GET_REGION_WORTH_FOR_PRODUCT: &str = r#" SELECT tpw.region_id, tpw.product_id, tpw.worth_percent FROM falukant_data.town_product_worth tpw JOIN falukant_data.branch b ON b.region_id = tpw.region_id WHERE b.falukant_user_id = $1 AND tpw.product_id = $2; "#; // Political offices and cumulative tax pub const QUERY_GET_USER_OFFICES: &str = r#" SELECT po.id AS office_id, pot.name AS office_name, po.region_id, rt.label_tr AS region_type FROM falukant_data.political_office po JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id JOIN falukant_data.region r ON r.id = po.region_id JOIN falukant_type.region rt ON rt.id = r.region_type_id JOIN falukant_data.character ch ON ch.id = po.character_id WHERE ch.user_id = $1 AND (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW(); "#; pub const QUERY_CUMULATIVE_TAX_NO_EXEMPT: &str = r#" WITH RECURSIVE ancestors AS ( SELECT id, parent_id, COALESCE(tax_percent,0.0) AS tax_percent FROM falukant_data.region WHERE id = $1 UNION ALL SELECT r.id, r.parent_id, COALESCE(r.tax_percent,0.0) FROM falukant_data.region r JOIN ancestors a ON r.id = a.parent_id ) SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors; "#; pub const QUERY_CUMULATIVE_TAX_WITH_EXEMPT: &str = r#" WITH RECURSIVE ancestors AS ( SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END AS tax_percent FROM falukant_data.region r JOIN falukant_type.region rt ON rt.id = r.region_type_id WHERE r.id = $1 UNION ALL SELECT r.id, r.parent_id, CASE WHEN rt.label_tr = ANY($2::text[]) THEN 0.0 ELSE COALESCE(r.tax_percent,0.0) END FROM falukant_data.region r JOIN falukant_type.region rt ON rt.id = r.region_type_id JOIN ancestors a ON r.id = a.parent_id ) SELECT COALESCE(SUM(tax_percent),0.0) AS total_percent FROM ancestors; "#; pub const QUERY_GET_TRANSPORT_VEHICLES_FOR_ROUTE: &str = r#" SELECT v.id AS vehicle_id, vt.capacity AS capacity FROM falukant_data.vehicle v JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id JOIN falukant_data.region_distance rd ON ((rd.source_region_id = v.region_id AND rd.target_region_id = $3) OR (rd.source_region_id = $3 AND rd.target_region_id = v.region_id)) AND (rd.transport_mode = vt.transport_mode OR rd.transport_mode IS NULL) WHERE v.falukant_user_id = $1 AND v.region_id = $2; "#; pub const QUERY_INSERT_TRANSPORT: &str = r#" INSERT INTO falukant_data.transport (source_region_id, target_region_id, product_id, size, vehicle_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()); "#; pub const QUERY_INSERT_EMPTY_TRANSPORT: &str = r#" INSERT INTO falukant_data.transport (source_region_id, target_region_id, product_id, size, vehicle_id, created_at, updated_at) VALUES ($1, $2, NULL, 0, $3, NOW(), NOW()); "#; pub const QUERY_GET_USER_BRANCHES: &str = r#" SELECT DISTINCT b.region_id, b.id AS branch_id FROM falukant_data.branch b WHERE b.falukant_user_id = $1 AND b.region_id != $2; "#; pub const QUERY_GET_FREE_VEHICLES_IN_REGION: &str = r#" SELECT v.id AS vehicle_id, vt.capacity AS capacity FROM falukant_data.vehicle v JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id WHERE v.falukant_user_id = $1 AND v.region_id = $2 AND v.id NOT IN (SELECT DISTINCT t.vehicle_id FROM falukant_data.transport t WHERE t.vehicle_id IS NOT NULL); "#; pub const QUERY_GET_SALARY_TO_PAY: &str = r#" SELECT d.id, d.employer_user_id, d.income FROM falukant_data.director d WHERE DATE(d.last_salary_payout) < DATE(NOW()); "#; pub const QUERY_SET_SALARY_PAYED: &str = r#" UPDATE falukant_data.director SET last_salary_payout = NOW() WHERE id = $1; "#; pub const QUERY_UPDATE_SATISFACTION: &str = r#" WITH new_sats AS ( SELECT d.id, ROUND(d.income::numeric / (c.title_of_nobility * POWER(1.231, AVG(k.knowledge) / 1.5)) * 100) AS new_satisfaction FROM falukant_data.director d JOIN falukant_data.knowledge k ON d.director_character_id = k.character_id JOIN falukant_data.character c ON c.id = d.director_character_id GROUP BY d.id, c.title_of_nobility, d.income ) UPDATE falukant_data.director dir SET satisfaction = ns.new_satisfaction FROM new_sats ns WHERE dir.id = ns.id AND dir.satisfaction IS DISTINCT FROM ns.new_satisfaction RETURNING dir.employer_user_id; "#; pub const QUERY_GET_DIRECTOR_USER: &str = r#" SELECT fu.id AS falukant_user_id FROM falukant_data.director d JOIN falukant_data.falukant_user fu ON fu.id = d.employer_user_id WHERE d.id = $1 LIMIT 1; "#; pub const QUERY_COUNT_VEHICLES_IN_BRANCH_REGION: &str = r#" SELECT COUNT(v.id) AS cnt FROM falukant_data.vehicle v WHERE v.falukant_user_id = $1 AND v.region_id = $2; "#; pub const QUERY_COUNT_VEHICLES_IN_REGION: &str = r#" SELECT COUNT(v.id) AS cnt FROM falukant_data.vehicle v WHERE v.falukant_user_id = $1 AND v.region_id = $2; "#; pub const QUERY_CHECK_ROUTE: &str = r#" SELECT 1 FROM falukant_data.region_distance rd WHERE (rd.source_region_id = $1 AND rd.target_region_id = $2) OR (rd.source_region_id = $2 AND rd.target_region_id = $1) LIMIT 1; "#; pub const QUERY_GET_BRANCH_REGION: &str = r#" SELECT region_id FROM falukant_data.branch WHERE id = $1 LIMIT 1; "#; pub const QUERY_GET_AVERAGE_WORTH: &str = r#" SELECT AVG(tpw.worth_percent) AS avg_worth FROM falukant_data.town_product_worth tpw WHERE tpw.product_id = $1 AND tpw.region_id IN (SELECT region_id FROM falukant_data.branch WHERE falukant_user_id = $2); "#; pub const QUERY_UPDATE_INVENTORY_QTY: &str = r#" UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; "#; pub const QUERY_GET_USER_STOCKS: &str = r#" SELECT s.id AS stock_id, s.quantity AS current_capacity FROM falukant_data.stock s JOIN falukant_data.branch b ON s.branch_id = b.id WHERE b.falukant_user_id = $1; "#; pub const QUERY_UPDATE_STOCK_CAPACITY: &str = r#" UPDATE falukant_data.stock SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) WHERE id = $2; "#; pub const QUERY_GET_REGION_STOCKS: &str = r#" SELECT s.id AS stock_id, s.quantity AS current_capacity FROM falukant_data.stock s JOIN falukant_data.branch b ON s.branch_id = b.id WHERE b.region_id = $1; "#; pub const QUERY_UPDATE_STOCK_CAPACITY_REGIONAL: &str = r#" UPDATE falukant_data.stock SET quantity = GREATEST(1, ROUND(quantity * (1 + $1 / 100.0))) WHERE id = $2; "#; // Stockage manager specific queries pub const QUERY_GET_TOWNS: &str = r#" SELECT fdr.id FROM falukant_data.region fdr JOIN falukant_type.region ftr ON ftr.id = fdr.region_type_id WHERE ftr.label_tr = 'city'; "#; pub const QUERY_INSERT_STOCK: &str = r#" INSERT INTO falukant_data.buyable_stock (region_id, stock_type_id, quantity) SELECT $1 AS region_id, s.id AS stock_type_id, GREATEST(1, ROUND(RANDOM() * 5 * COUNT(br.id))) AS quantity FROM falukant_data.branch AS br CROSS JOIN falukant_type.stock AS s WHERE br.region_id = $1 GROUP BY s.id ORDER BY RANDOM() LIMIT GREATEST( ROUND(RANDOM() * (SELECT COUNT(id) FROM falukant_type.stock)), 1 ); "#; pub const QUERY_CLEANUP_STOCK: &str = r#" DELETE FROM falukant_data.buyable_stock WHERE quantity <= 0; "#; pub const QUERY_GET_REGION_USERS: &str = r#" SELECT c.user_id FROM falukant_data.character c WHERE c.region_id = $1 AND c.user_id IS NOT NULL; "#; pub const QUERY_GET_REGION_HOUSES: &str = r#" SELECT uh.id AS house_id, uh.roof_condition, uh.floor_condition, uh.wall_condition, uh.window_condition FROM falukant_data.user_house uh JOIN falukant_data.character c ON c.user_id = uh.user_id WHERE c.region_id = $1 AND uh.house_type_id NOT IN ( SELECT id FROM falukant_type.house h WHERE h.label_tr = 'under_bridge' ); "#; // House worker queries pub const QUERY_GET_NEW_HOUSE_DATA: &str = r#" SELECT h.id AS house_id FROM falukant_type.house AS h WHERE random() < 0.01 AND label_tr <> 'under_bridge'; "#; pub const QUERY_ADD_NEW_BUYABLE_HOUSE: &str = r#" INSERT INTO falukant_data.buyable_house (house_type_id) VALUES ($1); "#; pub const QUERY_UPDATE_BUYABLE_HOUSE_STATE: &str = r#" UPDATE falukant_data.buyable_house SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), window_condition = ROUND(window_condition - random() * (3 + 0 * id)); "#; pub const QUERY_UPDATE_USER_HOUSE_STATE: &str = r#" UPDATE falukant_data.user_house SET roof_condition = ROUND(roof_condition - random() * (3 + 0 * id)), floor_condition = ROUND(floor_condition - random() * (3 + 0 * id)), wall_condition = ROUND(wall_condition - random() * (3 + 0 * id)), window_condition = ROUND(window_condition - random() * (3 + 0 * id)) WHERE house_type_id NOT IN ( SELECT id FROM falukant_type.house h WHERE h.label_tr = 'under_bridge' ); "#; pub const QUERY_UPDATE_HOUSE_QUALITY: &str = r#" UPDATE falukant_data.user_house SET roof_condition = GREATEST(0, LEAST(100, roof_condition + $1)), floor_condition = GREATEST(0, LEAST(100, floor_condition + $1)), wall_condition = GREATEST(0, LEAST(100, wall_condition + $1)), window_condition = GREATEST(0, LEAST(100, window_condition + $1)) WHERE id = $2; "#; pub const QUERY_CHANGE_WEATHER: &str = r#" UPDATE falukant_data.weather SET weather_type_id = ( SELECT id FROM falukant_type.weather ORDER BY RANDOM() LIMIT 1 ) WHERE region_id = $1; "#; pub const QUERY_GET_RANDOM_CHARACTER: &str = r#" SELECT id, health FROM falukant_data."character" WHERE user_id = $1 AND health > 0 ORDER BY RANDOM() LIMIT 1; "#; pub const QUERY_UPDATE_HEALTH: &str = r#" UPDATE falukant_data."character" SET health = $1 WHERE id = $2; "#; pub const QUERY_GET_REGION_CHARACTERS: &str = r#" SELECT id, health, user_id FROM falukant_data."character" WHERE region_id = $1 AND health > 0; "#; /// Anzeige für Todes-Logs (`falukant_log.notification.tr` als JSON): Name, Wohnort, Alter, Verknüpfungen. /// `region_id` = Ansässigkeit; `region_label` aus `falukant_data.region.name`, falls vorhanden, sonst `region_id` als Text. pub const QUERY_DEATH_LOG_CHARACTER_BASE: &str = r#" SELECT c.id AS character_id, COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), c.id::text) AS display_name, c.region_id, COALESCE(NULLIF(TRIM(dr.name::text), ''), c.region_id::text) AS region_label, GREATEST(0, FLOOR((CURRENT_DATE - c.birthdate::date) / 365.25))::int AS age_years FROM falukant_data.character c LEFT JOIN falukant_predefine.firstname fn ON fn.id = c.first_name LEFT JOIN falukant_predefine.lastname ln ON ln.id = c.last_name LEFT JOIN falukant_data.region dr ON dr.id = c.region_id WHERE c.id = $1::int; "#; pub const QUERY_DEATH_LOG_SPOUSE_DISPLAY_NAMES: &str = r#" SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), o.id::text) AS display_name FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr IN ('married', 'engaged', 'wooing') JOIN falukant_data.character o ON o.id = CASE WHEN r.character1_id = $1::int THEN r.character2_id ELSE r.character1_id END LEFT JOIN falukant_predefine.firstname fn ON fn.id = o.first_name LEFT JOIN falukant_predefine.lastname ln ON ln.id = o.last_name WHERE r.character1_id = $1::int OR r.character2_id = $1::int; "#; pub const QUERY_DEATH_LOG_CHILD_DISPLAY_NAMES: &str = r#" SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), ch.id::text) AS display_name FROM falukant_data.child_relation cr JOIN falukant_data.character ch ON ch.id = cr.child_character_id LEFT JOIN falukant_predefine.firstname fn ON fn.id = ch.first_name LEFT JOIN falukant_predefine.lastname ln ON ln.id = ch.last_name WHERE cr.father_character_id = $1::int OR cr.mother_character_id = $1::int; "#; /// Kinder mit `birth_context = 'lover'` (unehelich / aus Liebschaft); Teilmenge von `QUERY_DEATH_LOG_CHILD_DISPLAY_NAMES`. pub const QUERY_DEATH_LOG_CHILD_LOVER_BIRTH_DISPLAY_NAMES: &str = r#" SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), ch.id::text) AS display_name FROM falukant_data.child_relation cr JOIN falukant_data.character ch ON ch.id = cr.child_character_id LEFT JOIN falukant_predefine.firstname fn ON fn.id = ch.first_name LEFT JOIN falukant_predefine.lastname ln ON ln.id = ch.last_name WHERE (cr.father_character_id = $1::int OR cr.mother_character_id = $1::int) AND cr.birth_context = 'lover'; "#; /// Geliebte: Zeilen in `falukant_data.relationship`, Typ über `falukant_type.relationship` mit `tr = 'lover'` /// (kein Filter auf `relationship_state.lover_role` — `secret_affair` / `lover` / `mistress_or_favorite` sind nur Ausprägungen). /// „Aktiv“ wie Backend (`falukantService`): `relationship_state.active` darf nicht `false` sein (`IS NOT FALSE`). pub const QUERY_DEATH_LOG_LOVER_DISPLAY_NAMES: &str = r#" SELECT COALESCE(TRIM(BOTH FROM COALESCE(fn.label::text, '') || ' ' || COALESCE(ln.label::text, '')), o.id::text) AS display_name FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' JOIN falukant_data.relationship_state rs ON rs.relationship_id = r.id AND (rs.active IS NOT FALSE) JOIN falukant_data.character o ON o.id = CASE WHEN r.character1_id = $1::int THEN r.character2_id ELSE r.character1_id END LEFT JOIN falukant_predefine.firstname fn ON fn.id = o.first_name LEFT JOIN falukant_predefine.lastname ln ON ln.id = o.last_name WHERE r.character1_id = $1::int OR r.character2_id = $1::int; "#; pub const QUERY_DELETE_DIRECTOR: &str = r#" DELETE FROM falukant_data.director WHERE director_character_id = $1 RETURNING employer_user_id; "#; pub const QUERY_DELETE_RELATIONSHIP: &str = r#" WITH deleted AS ( DELETE FROM falukant_data.relationship WHERE character1_id = $1 OR character2_id = $1 RETURNING CASE WHEN character1_id = $1 THEN character2_id ELSE character1_id END AS related_character_id, relationship_type_id ) SELECT c.user_id AS related_user_id FROM deleted d JOIN falukant_data.character c ON c.id = d.related_character_id; "#; pub const QUERY_GET_USER_ID: &str = r#" SELECT user_id FROM falukant_data.character WHERE id = $1; "#; pub const QUERY_DELETE_CHILD_RELATION: &str = r#" WITH deleted AS ( DELETE FROM falukant_data.child_relation WHERE child_character_id = $1 RETURNING father_character_id, mother_character_id ) SELECT cf.user_id AS father_user_id, cm.user_id AS mother_user_id FROM deleted d JOIN falukant_data.character cf ON cf.id = d.father_character_id JOIN falukant_data.character cm ON cm.id = d.mother_character_id; "#; /// Entfernt alle child_relation-Zeilen, in denen der Charakter Vater oder Mutter ist (nötig vor Charakter-Löschung). pub const QUERY_DELETE_CHILD_RELATION_BY_PARENT: &str = r#" DELETE FROM falukant_data.child_relation WHERE father_character_id = $1 OR mother_character_id = $1; "#; pub const QUERY_DELETE_CHARACTER: &str = r#" DELETE FROM falukant_data.character WHERE id = $1; "#; pub const QUERY_GET_HEIR: &str = r#" SELECT child_character_id FROM falukant_data.child_relation WHERE father_character_id = $1 OR mother_character_id = $1 ORDER BY (is_heir IS TRUE) DESC, updated_at DESC LIMIT 1; "#; pub const QUERY_SET_CHARACTER_USER: &str = r#" UPDATE falukant_data.character SET user_id = $1, updated_at = NOW() WHERE id = $2; "#; pub const QUERY_GET_CURRENT_MONEY: &str = r#" SELECT money FROM falukant_data.falukant_user WHERE id = $1; "#; pub const QUERY_GET_HOUSE_VALUE: &str = r#" SELECT COALESCE(SUM(h.cost), 0) AS sum FROM falukant_data.user_house AS uh JOIN falukant_type.house AS h ON uh.house_type_id = h.id WHERE uh.user_id = $1; "#; pub const QUERY_GET_SETTLEMENT_VALUE: &str = r#" SELECT COALESCE(SUM(b.base_cost), 0) AS sum FROM falukant_data.branch AS br JOIN falukant_type.branch AS b ON br.branch_type_id = b.id WHERE br.falukant_user_id = $1; "#; pub const QUERY_GET_INVENTORY_VALUE: &str = r#" SELECT COALESCE(SUM(i.quantity * p.sell_cost), 0) AS sum FROM falukant_data.inventory AS i JOIN falukant_type.product AS p ON i.product_id = p.id JOIN falukant_data.stock AS s ON i.stock_id = s.id JOIN falukant_data.branch AS br ON s.branch_id = br.id WHERE br.falukant_user_id = $1; "#; pub const QUERY_GET_CREDIT_DEBT: &str = r#" SELECT COALESCE(SUM(remaining_amount), 0) AS sum FROM falukant_data.credit WHERE falukant_user_id = $1; "#; pub const QUERY_COUNT_CHILDREN: &str = r#" SELECT COUNT(*) AS cnt FROM falukant_data.child_relation WHERE (father_character_id = $1 OR mother_character_id = $1) AND child_character_id != $2; "#; /// Zählt Kinder eines Users (über alle Charaktere des Users als Elternteil). Ein Parameter: user_id. pub const QUERY_COUNT_CHILDREN_BY_USER: &str = r#" SELECT COUNT(DISTINCT cr.child_character_id) AS cnt FROM falukant_data.child_relation cr JOIN falukant_data.character parent ON (parent.id = cr.father_character_id OR parent.id = cr.mother_character_id) WHERE parent.user_id = $1; "#; // user_character worker queries pub const QUERY_GET_USERS_TO_UPDATE: &str = r#" SELECT id, CURRENT_DATE - birthdate::date AS age, health FROM falukant_data."character" WHERE user_id IS NOT NULL; "#; /// Spieler-Charaktere mit Health <= 0 werden periodisch zum Tod verarbeitet. pub const QUERY_GET_CHARACTERS_ZERO_HEALTH: &str = r#" SELECT id FROM falukant_data."character" WHERE user_id IS NOT NULL AND health <= 0; "#; // politics worker queries pub const QUERY_COUNT_OFFICES_PER_REGION: &str = r#" WITH seats_per_region AS ( SELECT pot.id AS office_type_id, rt.id AS region_id, pot.seats_per_region AS seats_total FROM falukant_type.political_office_type AS pot JOIN falukant_type.region AS rt ON pot.region_type = rt.label_tr ), occupied AS ( SELECT po.office_type_id, po.region_id, COUNT(*) AS occupied_count FROM falukant_data.political_office AS po GROUP BY po.office_type_id, po.region_id ), combined AS ( SELECT spr.region_id, spr.seats_total AS required_count, COALESCE(o.occupied_count, 0) AS occupied_count FROM seats_per_region AS spr LEFT JOIN occupied AS o ON spr.office_type_id = o.office_type_id AND spr.region_id = o.region_id ) SELECT region_id, SUM(required_count) AS required_count, SUM(occupied_count) AS occupied_count FROM combined GROUP BY region_id; "#; pub const QUERY_FIND_OFFICE_GAPS: &str = r#" WITH seats AS ( SELECT pot.id AS office_type_id, rt.id AS region_id, pot.seats_per_region AS seats_total FROM falukant_type.political_office_type AS pot JOIN falukant_type.region AS rt ON pot.region_type = rt.label_tr ), occupied AS ( SELECT po.office_type_id, po.region_id, COUNT(*) AS occupied_count FROM falukant_data.political_office AS po GROUP BY po.office_type_id, po.region_id ) SELECT s.office_type_id, s.region_id, (s.seats_total - COALESCE(o.occupied_count, 0)) AS gaps FROM seats AS s LEFT JOIN occupied AS o ON s.office_type_id = o.office_type_id AND s.region_id = o.region_id WHERE (s.seats_total - COALESCE(o.occupied_count, 0)) > 0; "#; pub const QUERY_SELECT_NEEDED_ELECTIONS: &str = r#" WITH target_date AS ( SELECT NOW()::date AS election_date ), expired_today AS ( DELETE FROM falukant_data.political_office AS po USING falukant_type.political_office_type AS pot WHERE po.office_type_id = pot.id AND (po.created_at + (pot.term_length * INTERVAL '1 day'))::date = (SELECT election_date FROM target_date) RETURNING pot.id AS office_type_id, po.region_id AS region_id ), gaps_per_region AS ( SELECT office_type_id, region_id, COUNT(*) AS gaps FROM expired_today GROUP BY office_type_id, region_id ), to_schedule AS ( SELECT g.office_type_id, g.region_id, g.gaps, td.election_date FROM gaps_per_region AS g CROSS JOIN target_date AS td WHERE NOT EXISTS ( SELECT 1 FROM falukant_data.election AS e WHERE e.office_type_id = g.office_type_id AND e.region_id = g.region_id AND e.date::date = td.election_date ) ), new_elections AS ( INSERT INTO falukant_data.election (office_type_id, date, posts_to_fill, created_at, updated_at, region_id) SELECT ts.office_type_id, ts.election_date, ts.gaps, NOW(), NOW(), ts.region_id FROM to_schedule AS ts RETURNING id AS election_id, region_id, posts_to_fill ) SELECT ne.election_id, ne.region_id, ne.posts_to_fill FROM new_elections AS ne ORDER BY ne.region_id, ne.election_id; "#; pub const QUERY_INSERT_CANDIDATES: &str = r#" INSERT INTO falukant_data.candidate (election_id, character_id, created_at, updated_at) SELECT $1 AS election_id, sub.id AS character_id, NOW() AS created_at, NOW() AS updated_at FROM ( WITH RECURSIVE region_tree AS ( SELECT r.id FROM falukant_data.region AS r WHERE r.id = $2 UNION ALL SELECT r2.id FROM falukant_data.region AS r2 JOIN region_tree AS rt ON r2.parent_id = rt.id ) SELECT ch.id FROM falukant_data.character AS ch JOIN region_tree AS rt2 ON ch.region_id = rt2.id WHERE ch.user_id IS NULL AND ch.birthdate <= NOW() - INTERVAL '21 days' AND ch.title_of_nobility IN ( SELECT id FROM falukant_type.title WHERE label_tr != 'noncivil' ) ORDER BY RANDOM() LIMIT ($3 * 2) ) AS sub(id); "#; pub const QUERY_SELECT_ELECTIONS_NEEDING_CANDIDATES: &str = r#" SELECT e.id AS election_id, e.region_id AS region_id, e.posts_to_fill FROM falukant_data.election AS e WHERE e.region_id IS NOT NULL AND e.posts_to_fill > 0 AND e.date::date >= CURRENT_DATE AND NOT EXISTS ( SELECT 1 FROM falukant_data.candidate AS c WHERE c.election_id = e.id ); "#; pub const QUERY_PROCESS_EXPIRED_AND_FILL: &str = r#" WITH expired_offices AS ( DELETE FROM falukant_data.political_office AS po USING falukant_type.political_office_type AS pot WHERE po.office_type_id = pot.id AND (po.created_at + (pot.term_length * INTERVAL '1 day')) <= NOW() RETURNING pot.id AS office_type_id, po.region_id AS region_id ), distinct_types AS ( SELECT DISTINCT office_type_id, region_id FROM expired_offices ), votes_per_candidate AS ( SELECT dt.office_type_id, dt.region_id, c.character_id, COUNT(v.id) AS vote_count FROM distinct_types AS dt JOIN falukant_data.election AS e ON e.office_type_id = dt.office_type_id JOIN falukant_data.vote AS v ON v.election_id = e.id JOIN falukant_data.candidate AS c ON c.election_id = e.id AND c.id = v.candidate_id WHERE e.date >= (NOW() - INTERVAL '30 days') GROUP BY dt.office_type_id, dt.region_id, c.character_id ), ranked_winners AS ( SELECT vpc.office_type_id, vpc.region_id, vpc.character_id, ROW_NUMBER() OVER ( PARTITION BY vpc.office_type_id, vpc.region_id ORDER BY vpc.vote_count DESC ) AS rn FROM votes_per_candidate AS vpc ), selected_winners AS ( SELECT rw.office_type_id, rw.region_id, rw.character_id FROM ranked_winners AS rw JOIN falukant_type.political_office_type AS pot ON pot.id = rw.office_type_id WHERE rw.rn <= pot.seats_per_region ), insert_winners AS ( INSERT INTO falukant_data.political_office (office_type_id, character_id, created_at, updated_at, region_id) SELECT sw.office_type_id, sw.character_id, NOW(), NOW(), sw.region_id FROM selected_winners AS sw RETURNING id AS new_office_id, office_type_id, character_id, region_id ), count_inserted AS ( SELECT office_type_id, region_id, COUNT(*) AS inserted_count FROM insert_winners GROUP BY office_type_id, region_id ), needed_to_fill AS ( SELECT dt.office_type_id, dt.region_id, (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) AS gaps FROM distinct_types AS dt JOIN falukant_type.political_office_type AS pot ON pot.id = dt.office_type_id LEFT JOIN count_inserted AS ci ON ci.office_type_id = dt.office_type_id AND ci.region_id = dt.region_id WHERE (pot.seats_per_region - COALESCE(ci.inserted_count, 0)) > 0 ), random_candidates AS ( SELECT rtf.office_type_id, rtf.region_id, ch.id AS character_id, ROW_NUMBER() OVER ( PARTITION BY rtf.office_type_id, rtf.region_id ORDER BY RANDOM() ) AS rn FROM needed_to_fill AS rtf JOIN falukant_data.character AS ch ON ch.region_id IN ( WITH RECURSIVE region_tree AS ( SELECT id FROM falukant_data.region WHERE id = rtf.region_id UNION ALL SELECT r2.id FROM falukant_data.region r2 JOIN region_tree rt ON r2.parent_id = rt.id ) SELECT id FROM region_tree ) AND ch.user_id IS NULL AND ch.birthdate <= NOW() - INTERVAL '21 days' AND ch.title_of_nobility IN ( SELECT id FROM falukant_type.title WHERE label_tr != 'noncivil' ) AND NOT EXISTS ( SELECT 1 FROM falukant_data.political_office AS po2 JOIN falukant_type.political_office_type AS pot2 ON pot2.id = po2.office_type_id WHERE po2.character_id = ch.id AND (po2.created_at + (pot2.term_length * INTERVAL '1 day')) > NOW() + INTERVAL '2 days' ) ), insert_random AS ( INSERT INTO falukant_data.political_office (office_type_id, character_id, created_at, updated_at, region_id) SELECT rc.office_type_id, rc.character_id, NOW(), NOW(), rc.region_id FROM random_candidates AS rc JOIN needed_to_fill AS rtf ON rtf.office_type_id = rc.office_type_id AND rtf.region_id = rc.region_id WHERE rc.rn <= rtf.gaps RETURNING id AS new_office_id, office_type_id, character_id, region_id ) SELECT new_office_id AS office_id, office_type_id, character_id, region_id FROM insert_winners UNION ALL SELECT new_office_id AS office_id, office_type_id, character_id, region_id FROM insert_random; "#; pub const QUERY_USERS_IN_CITIES_OF_REGIONS: &str = r#" WITH RECURSIVE region_tree AS ( SELECT id FROM falukant_data.region WHERE id = $1 UNION ALL SELECT r2.id FROM falukant_data.region AS r2 JOIN region_tree AS rt ON r2.parent_id = rt.id ) SELECT DISTINCT ch.user_id FROM falukant_data.character AS ch JOIN region_tree AS rt2 ON ch.region_id = rt2.id WHERE ch.user_id IS NOT NULL; "#; pub const QUERY_NOTIFY_OFFICE_EXPIRATION: &str = r#" INSERT INTO falukant_log.notification (user_id, tr, created_at, updated_at) SELECT ch.user_id, 'notify_office_expiring', NOW(), NOW() FROM falukant_data.political_office AS po JOIN falukant_type.political_office_type AS pot ON po.office_type_id = pot.id JOIN falukant_data.character AS ch ON ch.id = po.character_id WHERE ch.user_id IS NOT NULL AND (po.created_at + (pot.term_length * INTERVAL '1 day')) BETWEEN (NOW() + INTERVAL '2 days') AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); "#; pub const QUERY_NOTIFY_ELECTION_CREATED: &str = r#" INSERT INTO falukant_log.notification (user_id, tr, created_at, updated_at) VALUES ($1, 'notify_election_created', NOW(), NOW()); "#; pub const QUERY_NOTIFY_OFFICE_FILLED: &str = r#" INSERT INTO falukant_log.notification (user_id, tr, created_at, updated_at) VALUES ((SELECT user_id FROM falukant_data.character WHERE id = $1), 'notify_office_filled', NOW(), NOW()); "#; pub const QUERY_GET_USERS_WITH_EXPIRING_OFFICES: &str = r#" SELECT DISTINCT ch.user_id FROM falukant_data.political_office AS po JOIN falukant_type.political_office_type AS pot ON po.office_type_id = pot.id JOIN falukant_data.character AS ch ON po.character_id = ch.id WHERE ch.user_id IS NOT NULL AND (po.created_at + (pot.term_length * INTERVAL '1 day')) BETWEEN (NOW() + INTERVAL '2 days') AND (NOW() + INTERVAL '2 days' + INTERVAL '1 second'); "#; pub const QUERY_GET_USERS_IN_REGIONS_WITH_ELECTIONS: &str = r#" SELECT DISTINCT ch.user_id FROM falukant_data.election AS e JOIN falukant_data.character AS ch ON ch.region_id = e.region_id WHERE ch.user_id IS NOT NULL AND e.date >= NOW() - INTERVAL '1 day'; "#; pub const QUERY_GET_USERS_WITH_FILLED_OFFICES: &str = r#" SELECT DISTINCT ch.user_id FROM falukant_data.political_office AS po JOIN falukant_data.character AS ch ON po.character_id = ch.id WHERE ch.user_id IS NOT NULL AND po.created_at >= NOW() - INTERVAL '1 minute'; "#; pub const QUERY_PROCESS_ELECTIONS: &str = r#" SELECT office_id, office_type_id, character_id, region_id FROM falukant_data.process_elections(); "#; /// Wahlergebnis für Spieler-Kandidaten (`falukant_data.candidate` + `character.user_id IS NOT NULL`). /// Läuft nach `process_elections()`; Deduplizierung über bestehende `election_result`-Notifications (`tr` JSON). pub const QUERY_PLAYER_ELECTION_RESULT_ROWS: &str = r#" WITH vote_counts AS ( SELECT c.id AS candidate_id, c.election_id, c.character_id, COUNT(v.id)::bigint AS vote_count FROM falukant_data.candidate c LEFT JOIN falukant_data.vote v ON v.candidate_id = c.id GROUP BY c.id, c.election_id, c.character_id ), top_per_election AS ( SELECT DISTINCT ON (vc.election_id) vc.election_id, vc.character_id AS top_character_id, vc.vote_count AS top_votes FROM vote_counts vc ORDER BY vc.election_id, vc.vote_count DESC, vc.candidate_id ), eligible AS ( SELECT e.id AS election_id, e.office_type_id, e.region_id, e.posts_to_fill, vc.candidate_id, vc.character_id, vc.vote_count, ch.user_id FROM falukant_data.election e JOIN vote_counts vc ON vc.election_id = e.id JOIN falukant_data.character ch ON ch.id = vc.character_id WHERE ch.user_id IS NOT NULL AND e.date::date <= CURRENT_DATE AND e.date::date >= CURRENT_DATE - INTERVAL '7 days' AND NOT EXISTS ( SELECT 1 FROM falukant_log.notification n WHERE n.user_id = ch.user_id AND n.tr LIKE '{%' AND (n.tr::jsonb->>'event') = 'election_result' AND (n.tr::jsonb->>'election_id')::int = e.id ) ), ranked AS ( SELECT vc.candidate_id, ROW_NUMBER() OVER ( PARTITION BY vc.election_id ORDER BY vc.vote_count DESC, vc.candidate_id ) AS rank_in_election FROM vote_counts vc ) SELECT elig.user_id, elig.election_id, elig.office_type_id, COALESCE(pot.name::text, '') AS office_name, elig.region_id, COALESCE(NULLIF(TRIM(dr.name::text), ''), elig.region_id::text) AS region_name, elig.character_id, elig.candidate_id, elig.vote_count AS your_votes, COALESCE(rk.rank_in_election, 0)::int AS rank_in_election, EXISTS ( SELECT 1 FROM falukant_data.political_office po WHERE po.character_id = elig.character_id AND po.office_type_id = elig.office_type_id AND po.region_id = elig.region_id AND po.created_at >= (CURRENT_TIMESTAMP - INTERVAL '3 days') ) AS won, tpe.top_character_id, tpe.top_votes, elig.posts_to_fill FROM eligible elig JOIN falukant_type.political_office_type pot ON pot.id = elig.office_type_id LEFT JOIN falukant_data.region dr ON dr.id = elig.region_id LEFT JOIN top_per_election tpe ON tpe.election_id = elig.election_id LEFT JOIN ranked rk ON rk.candidate_id = elig.candidate_id ORDER BY elig.election_id, elig.user_id; "#; // --- Politische Amtsvorteile (reputation_periodic, free_lover_slots, Ernennungs-Ablauf) --- /// `political_benefit_last_tick` + `falukant_predefine.political_office_benefit` vorhanden. pub const QUERY_POLITICAL_BENEFIT_DAEMON_SCHEMA_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'falukant_data' AND table_name = 'political_benefit_last_tick' ) AND EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'falukant_predefine' AND table_name = 'political_office_benefit' ) AS ready; "#; /// Spieler mit aktivem Amt + `reputation_periodic` (JSON `tr` oder `benefitType`); Kalendertage seit `last_tick_at`. pub const QUERY_POLITICAL_REPUTATION_TICK_ROWS: &str = r#" SELECT pob.id AS benefit_id, po.character_id AS character_id, GREATEST(1, COALESCE(NULLIF((pob.value::jsonb->>'intervalDays'), '')::int, 7)) AS interval_days, COALESCE(NULLIF((pob.value::jsonb->>'gain'), '')::numeric, 0::numeric) AS gain, ch.user_id AS falukant_user_id FROM falukant_data.political_office po JOIN falukant_predefine.political_office_benefit pob ON pob.political_office_type_id = po.office_type_id JOIN falukant_data.character ch ON ch.id = po.character_id LEFT JOIN falukant_data.political_benefit_last_tick btl ON btl.character_id = po.character_id AND btl.political_office_benefit_id = pob.id WHERE po.character_id IS NOT NULL AND ch.user_id IS NOT NULL AND COALESCE(pob.value::jsonb->>'tr', pob.value::jsonb->>'benefitType') = 'reputation_periodic' AND COALESCE(NULLIF((pob.value::jsonb->>'gain'), '')::numeric, 0::numeric) > 0 AND ( btl.last_tick_at IS NULL OR (CURRENT_DATE - (btl.last_tick_at::date)) >= GREATEST(1, COALESCE(NULLIF((pob.value::jsonb->>'intervalDays'), '')::int, 7)) ); "#; pub const QUERY_POLITICAL_REPUTATION_TICK_UPSERT: &str = r#" INSERT INTO falukant_data.political_benefit_last_tick (character_id, political_office_benefit_id, last_tick_at, ticks_count) VALUES ($1::int, $2::int, NOW(), 1) ON CONFLICT (character_id, political_office_benefit_id) DO UPDATE SET last_tick_at = EXCLUDED.last_tick_at, ticks_count = falukant_data.political_benefit_last_tick.ticks_count + 1; "#; pub const QUERY_POLITICAL_REPUTATION_APPLY_GAIN: &str = r#" UPDATE falukant_data.character c SET reputation = LEAST( 100::numeric, GREATEST(0::numeric, COALESCE(c.reputation, 50::numeric) + $gain::numeric) ), updated_at = NOW() WHERE c.id = $1::int; "#; pub const QUERY_POLITICAL_APPOINTMENT_SCHEMA_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = 'falukant_data' AND table_name = 'political_appointment' ) AS ready; "#; pub const QUERY_POLITICAL_APPOINTMENT_EXPIRE_PENDING: &str = r#" UPDATE falukant_data.political_appointment SET status = 'expired', updated_at = NOW() WHERE status = 'pending' AND expires_at IS NOT NULL AND expires_at < NOW(); "#; /// Summe `count` aus `free_lover_slots`-Benefits (JSON `tr`/`benefitType`), gedeckelt. pub const QUERY_SUM_FREE_LOVER_SLOTS_FOR_CHARACTER: &str = r#" SELECT LEAST( 5, COALESCE(SUM( GREATEST(0, COALESCE(NULLIF((pob.value::jsonb->>'count'), '')::int, 0)) ), 0) )::int AS free_slots FROM falukant_data.political_office po JOIN falukant_predefine.political_office_benefit pob ON pob.political_office_type_id = po.office_type_id WHERE po.character_id = $1::int AND COALESCE(pob.value::jsonb->>'tr', pob.value::jsonb->>'benefitType') = 'free_lover_slots'; "#; pub const QUERY_TRIM_EXCESS_OFFICES_GLOBAL: &str = r#" WITH seats AS ( SELECT pot.id AS office_type_id, rt.id AS region_id, pot.seats_per_region AS seats_total FROM falukant_type.political_office_type AS pot JOIN falukant_type.region AS rt ON pot.region_type = rt.label_tr ), ranked AS ( SELECT po.id, po.office_type_id, po.region_id, s.seats_total, ROW_NUMBER() OVER ( PARTITION BY po.office_type_id, po.region_id ORDER BY po.created_at DESC ) AS rn FROM falukant_data.political_office AS po JOIN seats AS s ON s.office_type_id = po.office_type_id AND s.region_id = po.region_id ), to_delete AS ( SELECT id FROM ranked WHERE rn > seats_total ) DELETE FROM falukant_data.political_office WHERE id IN (SELECT id FROM to_delete); "#; pub const QUERY_UPDATE_CHARACTERS_HEALTH: &str = r#" UPDATE falukant_data."character" SET health = $1 WHERE id = $2; "#; pub const QUERY_UPDATE_MOOD: &str = r#" UPDATE falukant_data."character" AS c SET mood_id = falukant_data.get_random_mood_id() WHERE c.health > 0 AND random() < (1.0 / 50.0); "#; pub const QUERY_UPDATE_GET_ITEMS_TO_UPDATE: &str = r#" SELECT id, product_id, producer_id, quantity FROM falukant_log.production p WHERE p.production_timestamp::date < current_date; "#; pub const QUERY_UPDATE_GET_CHARACTER_IDS: &str = r#" SELECT fu.id AS user_id, c.id AS character_id, c2.id AS director_id FROM falukant_data.falukant_user fu JOIN falukant_data.character c ON c.user_id = fu.id LEFT JOIN falukant_data.director d ON d.employer_user_id = fu.id LEFT JOIN falukant_data.character c2 ON c2.id = d.director_character_id WHERE fu.id = $1; "#; pub const QUERY_UPDATE_KNOWLEDGE: &str = r#" UPDATE falukant_data.knowledge SET knowledge = LEAST(knowledge + $3, 100) WHERE character_id = $1 AND product_id = $2; "#; pub const QUERY_DELETE_LOG_ENTRY: &str = r#" DELETE FROM falukant_log.production WHERE id = $1; "#; pub const QUERY_GET_OPEN_CREDITS: &str = r#" SELECT c.id AS credit_id, c.amount, c.remaining_amount, c.interest_rate, fu.id AS user_id, fu.money, c2.id AS character_id, dp.created_at AS debitor_prism_start, dp.created_at::date < current_date AS prism_started_previously FROM falukant_data.credit c JOIN falukant_data.falukant_user fu ON fu.id = c.falukant_user_id JOIN falukant_data.character c2 ON c2.user_id = c.falukant_user_id LEFT JOIN falukant_data.debtors_prism dp ON dp.character_id = c2.id WHERE c.remaining_amount > 0 AND c.updated_at::date < current_date; "#; pub const QUERY_UPDATE_CREDIT: &str = r#" UPDATE falukant_data.credit c SET remaining_amount = $1 WHERE falukant_user_id = $2; "#; pub const QUERY_CLEANUP_CREDITS: &str = r#" DELETE FROM falukant_data.credit WHERE remaining_amount <= 0.01; "#; // --- Falukant: Schuldturm & Pfändung (docs/FALUKANT_DEBTORS_DAEMON.md) --- pub const QUERY_DEBTORS_PRISM_SCHEMA_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'debtors_prism' AND column_name = 'days_overdue' ) AS ready; "#; pub const QUERY_DEBTORS_CREDIT_USERS_FOR_DAILY: &str = r#" SELECT c.falukant_user_id AS user_id, MIN(ch.id) AS character_id, COALESCE(fu.money, 0)::float8 AS money, COALESCE(SUM( c.amount::float8 / 10.0 + c.amount::float8 * c.interest_rate::float8 / 100.0 ), 0)::float8 AS total_pay_rate, COALESCE(SUM(c.remaining_amount), 0)::float8 AS total_credit_remaining FROM falukant_data.credit c JOIN falukant_data.falukant_user fu ON fu.id = c.falukant_user_id JOIN falukant_data.character ch ON ch.user_id = c.falukant_user_id AND ch.health > 0 WHERE c.remaining_amount > 0.01 GROUP BY c.falukant_user_id, fu.money; "#; pub const QUERY_DEBTORS_GET_PRISM_BY_CHARACTER: &str = r#" SELECT dp.id, dp.character_id, COALESCE(dp.status, '') AS status, COALESCE(dp.days_overdue, 0)::int AS days_overdue, COALESCE(dp.remaining_debt, 0)::float8 AS remaining_debt, dp.entered_at::text AS entered_at, dp.released_at::text AS released_at FROM falukant_data.debtors_prism dp WHERE dp.character_id = $1::int LIMIT 1; "#; pub const QUERY_DEBTORS_INSERT_DELINQUENT: &str = r#" INSERT INTO falukant_data.debtors_prism ( character_id, status, days_overdue, remaining_debt, next_forced_action, reason, creditworthiness_penalty, public_known, assets_seized_json ) SELECT $1::int, 'delinquent', 1, $2::float8, 'reminder', 'delinquent', 0, false, '{}'::jsonb WHERE NOT EXISTS ( SELECT 1 FROM falukant_data.debtors_prism d2 WHERE d2.character_id = $1::int ) RETURNING id; "#; pub const QUERY_DEBTORS_INCREMENT_DELINQUENT: &str = r#" UPDATE falukant_data.debtors_prism SET days_overdue = COALESCE(days_overdue, 0) + 1, remaining_debt = $2::float8, next_forced_action = CASE WHEN COALESCE(days_overdue, 0) + 1 >= 3 THEN 'asset_seizure' WHEN COALESCE(days_overdue, 0) + 1 = 2 THEN 'final_warning' ELSE 'reminder' END, updated_at = NOW() WHERE character_id = $1::int AND status = 'delinquent' RETURNING id, COALESCE(days_overdue, 0) AS new_days; "#; /// Nach abgeschlossenem Fall (`released`) neuer Verzug: Zeile wieder auf Delinquent setzen. pub const QUERY_DEBTORS_REACTIVATE_DELINQUENT_FROM_RELEASED: &str = r#" UPDATE falukant_data.debtors_prism SET status = 'delinquent', days_overdue = 1, remaining_debt = $2::float8, next_forced_action = 'reminder', reason = 'delinquent', updated_at = NOW() WHERE character_id = $1::int AND status = 'released' RETURNING id; "#; pub const QUERY_DEBTORS_RESET_DELINQUENCY_SOLVENT: &str = r#" UPDATE falukant_data.debtors_prism SET days_overdue = 0, next_forced_action = 'reminder', updated_at = NOW() WHERE character_id = $1::int AND status = 'delinquent'; "#; pub const QUERY_DEBTORS_RESET_ON_PAYMENT_SUCCESS: &str = r#" UPDATE falukant_data.debtors_prism SET days_overdue = 0, next_forced_action = 'reminder', updated_at = NOW() WHERE character_id = $1::int AND status = 'delinquent'; "#; pub const QUERY_DEBTORS_ENTER_PRISON: &str = r#" UPDATE falukant_data.debtors_prism SET status = 'imprisoned', entered_at = COALESCE(entered_at, NOW()), released_at = NULL, debt_at_entry = $2::float8, remaining_debt = $2::float8, reason = 'credit_default', creditworthiness_penalty = COALESCE(creditworthiness_penalty, 0) + 45, next_forced_action = 'asset_seizure', public_known = true, updated_at = NOW() WHERE character_id = $1::int AND status = 'delinquent' AND COALESCE(days_overdue, 0) >= 3 RETURNING id; "#; pub const QUERY_DEBTORS_RELEASE_IF_PAID: &str = r#" UPDATE falukant_data.debtors_prism dp SET status = 'released', released_at = NOW(), next_forced_action = NULL, days_overdue = 0, remaining_debt = 0, updated_at = NOW() FROM falukant_data.character ch WHERE ch.id = dp.character_id AND ch.user_id = $1::int AND dp.status = 'imprisoned' AND COALESCE(( SELECT SUM(c.remaining_amount) FROM falukant_data.credit c WHERE c.falukant_user_id = $1::int ), 0) <= 0.01 RETURNING dp.character_id; "#; pub const QUERY_DEBTORS_SUBTRACT_REPUTATION: &str = r#" UPDATE falukant_data.character SET reputation = GREATEST(0::numeric, COALESCE(reputation, 50::numeric) - $2::numeric), updated_at = NOW() WHERE id = $1::int AND user_id IS NOT NULL; "#; pub const QUERY_DEBTORS_MARRIAGE_SATISFACTION_SUB: &str = r#" UPDATE falukant_data.relationship r SET marriage_satisfaction = GREATEST(0, COALESCE(r.marriage_satisfaction, 55) - $2::int), updated_at = NOW() FROM falukant_type.relationship rt WHERE rt.id = r.relationship_type_id AND rt.tr IN ('married', 'engaged', 'wooing') AND (r.character1_id = $1::int OR r.character2_id = $1::int); "#; pub const QUERY_DEBTORS_HOUSEHOLD_TENSION_ADD: &str = r#" UPDATE falukant_data.user_house SET household_tension_score = LEAST(100, COALESCE(household_tension_score, 0) + $2::int), updated_at = NOW() WHERE user_id = $1::int; "#; pub const QUERY_DEBTORS_LOVER_AFFECTION_SUB: &str = r#" UPDATE falukant_data.relationship_state rs SET affection = GREATEST(0, COALESCE(rs.affection, 50) - $2::int), updated_at = NOW() FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' WHERE rs.relationship_id = r.id AND rs.active = true AND (r.character1_id = $1::int OR r.character2_id = $1::int); "#; pub const QUERY_DEBTORS_IMPRISONED_REP_MALUS: &str = r#" UPDATE falukant_data.character c SET reputation = GREATEST(0::numeric, COALESCE(c.reputation, 50::numeric) - $2::numeric), updated_at = NOW() WHERE c.id = $1::int AND c.user_id IS NOT NULL; "#; pub const QUERY_DEBTORS_IMPRISONED_PENALTY_PLUS1: &str = r#" UPDATE falukant_data.debtors_prism SET creditworthiness_penalty = COALESCE(creditworthiness_penalty, 0) + 1, updated_at = NOW() WHERE character_id = $1::int AND status = 'imprisoned'; "#; pub const QUERY_DEBTORS_IMPRISONED_MARRIAGE_SUB1: &str = r#" UPDATE falukant_data.relationship r SET marriage_satisfaction = GREATEST(0, COALESCE(r.marriage_satisfaction, 55) - 1), updated_at = NOW() FROM falukant_type.relationship rt WHERE rt.id = r.relationship_type_id AND rt.tr IN ('married', 'engaged', 'wooing') AND (r.character1_id = $1::int OR r.character2_id = $1::int); "#; pub const QUERY_DEBTORS_IMPRISONED_TENSION_PLUS2: &str = r#" UPDATE falukant_data.user_house uh SET household_tension_score = LEAST(100, COALESCE(uh.household_tension_score, 0) + 2), updated_at = NOW() FROM falukant_data.character ch WHERE ch.id = $1::int AND uh.user_id = ch.user_id; "#; pub const QUERY_DEBTORS_IMPRISONED_LOVER_AFF_SUB2: &str = r#" UPDATE falukant_data.relationship_state rs SET affection = GREATEST(0, COALESCE(rs.affection, 50) - 2), updated_at = NOW() FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' WHERE rs.relationship_id = r.id AND rs.active = true AND (r.character1_id = $1::int OR r.character2_id = $1::int); "#; pub const QUERY_DEBTORS_VEHICLE_FOR_SEIZURE: &str = r#" SELECT v.id AS vehicle_id, COALESCE(v.condition, 100)::int AS veh_condition, COALESCE(vt.cost, 0)::float8 AS type_cost FROM falukant_data.vehicle v JOIN falukant_type.vehicle vt ON vt.id = v.vehicle_type_id WHERE v.falukant_user_id = $1::int AND v.id NOT IN ( SELECT DISTINCT t.vehicle_id FROM falukant_data.transport t WHERE t.vehicle_id IS NOT NULL ) ORDER BY COALESCE(vt.cost, 0) ASC, v.id ASC LIMIT 1; "#; pub const QUERY_DEBTORS_DELETE_VEHICLE: &str = r#" DELETE FROM falukant_data.vehicle WHERE id = $1::int RETURNING id; "#; pub const QUERY_DEBTORS_UPDATE_PRISM_REMAINING: &str = r#" UPDATE falukant_data.debtors_prism dp SET remaining_debt = GREATEST(0, COALESCE(dp.remaining_debt, 0) - $2::float8), assets_seized_json = COALESCE(assets_seized_json, '{}'::jsonb) || jsonb_build_object( 'last_vehicle', jsonb_build_object('vehicle_id', $3::int, 'proceeds', $2::float8) ), updated_at = NOW() WHERE dp.character_id = $1::int AND dp.status = 'imprisoned' RETURNING dp.remaining_debt; "#; pub const QUERY_DEBTORS_UPDATE_PRISM_REMAINING_MONEY: &str = r#" UPDATE falukant_data.debtors_prism dp SET remaining_debt = GREATEST(0, COALESCE(dp.remaining_debt, 0) - $2::float8), assets_seized_json = COALESCE(assets_seized_json, '{}'::jsonb) || jsonb_build_object('last_cash_seizure', $2::float8), updated_at = NOW() WHERE dp.character_id = $1::int AND dp.status = 'imprisoned' RETURNING dp.remaining_debt; "#; pub const QUERY_RANDOM_HEIR: &str = r#" WITH chosen AS ( SELECT cr.id AS relation_id, cr.child_character_id FROM falukant_data.child_relation AS cr JOIN falukant_data.character AS ch ON ch.id = cr.child_character_id WHERE (cr.father_character_id = $1 OR cr.mother_character_id = $1) AND ch.region_id = ( SELECT region_id FROM falukant_data.character WHERE id = $1 ) AND ch.birthdate >= NOW() - INTERVAL '10 days' AND ch.title_of_nobility = ( SELECT id FROM falukant_type.title WHERE label_tr = 'noncivil' ) ORDER BY RANDOM() LIMIT 1 ) UPDATE falukant_data.child_relation AS cr2 SET is_heir = TRUE, updated_at = NOW() FROM chosen WHERE cr2.id = chosen.relation_id RETURNING chosen.child_character_id; "#; /// Aktualisiert den Geldstand eines Users. $1 = Geld als Text (z. B. "1234.56"), wird als numeric gecastet; $2 = user id. /// Verwendung von Text-Parameter vermeidet "error serializing parameter 0" bei f64/numeric. pub const QUERY_UPDATE_USER_MONEY: &str = r#" UPDATE falukant_data.falukant_user SET money = $1::numeric, updated_at = NOW() WHERE id = $2; "#; pub const QUERY_GET_FALUKANT_USER_ID: &str = r#" SELECT user_id FROM falukant_data.character WHERE id = $1 LIMIT 1; "#; pub const QUERY_AUTOBATISM: &str = r#" UPDATE falukant_data.child_relation SET name_set = TRUE WHERE id IN ( SELECT cr.id FROM falukant_data.child_relation cr JOIN falukant_data.character c ON c.id = cr.child_character_id WHERE cr.name_set = FALSE AND c.birthdate < current_date - INTERVAL '5 days' ); "#; // Biologische Fruchtbarkeit nach Alter der Frau (Jahres-Wahrscheinlichkeit prob_year). // Konzeption: genau ein Wurf pro Ehe und Kalendertag (`UserCharacterWorker`), mit random() < prob_year. // Entspricht „ein Spieljahr pro Kalendertag“: keine 24× stündlichen Versuche mehr, keine „Hochzeitsnacht“ // als Extra-Event — erste mögliche Konzeption am nächsten täglichen Fertilitätslauf nach der Hochzeit. // Grenzen in Tagen: 1 Jahr ≈ 365 Tage (mother_age_days). // Erstes gemeinsames Kind (0 Zeilen in child_relation für dieses Paar): prob_year mindestens 1.0 // im Alter 18–~44 (6570–16000 Tage), damit kinderlose Ehen nicht über viele Jahre ohne Nachwuchs bleiben. // WICHTIG: Vater/Mutter und Alter immer über gender ableiten — nicht character1/2 fest als Mutter! // // Ablauf Ehe: (1) Konzeption setzt `marriage_pregnancy_due_at` (+5 Tage), (2) Geburt wenn fällig. // Migration: `008_falukant_marriage_pregnancy_due.sql`. /// Fällige Geburten (Ehe): `marriage_pregnancy_due_at <= NOW()`. /// B2: keine Ehe-Geburt, solange die Mutter eine **geplante** Schwangerschaft (`character.pregnancy_due_at`) hat. pub const QUERY_GET_MARRIAGE_BIRTH_DELIVERIES: &str = r#" SELECT r.id AS relationship_id, CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END AS father_cid, CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END AS mother_cid, CASE WHEN c1.gender = 'male' THEN c1.title_of_nobility ELSE c2.title_of_nobility END AS title_of_nobility, CASE WHEN c1.gender = 'male' THEN c1.last_name ELSE c2.last_name END AS last_name, CASE WHEN c1.gender = 'male' THEN c1.region_id ELSE c2.region_id END AS region_id, CASE WHEN c1.gender = 'male' THEN fu1.id ELSE fu2.id END AS father_uid, CASE WHEN c1.gender = 'female' THEN fu1.id ELSE fu2.id END AS mother_uid, (CURRENT_DATE - c_female.birthdate::date)::int AS mother_age_days FROM falukant_data.relationship r JOIN falukant_type.relationship r2 ON r2.id = r.relationship_type_id AND r2.tr = 'married' JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id JOIN falukant_data.character c_female ON c_female.id = ( CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END ) LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id WHERE r.marriage_pregnancy_due_at IS NOT NULL AND r.marriage_pregnancy_due_at <= NOW() AND ((c1.gender = 'male' AND c2.gender = 'female') OR (c1.gender = 'female' AND c2.gender = 'male')) AND c_female.pregnancy_due_at IS NULL; "#; pub const QUERY_CLEAR_MARRIAGE_PREGNANCY_DUE: &str = r#" UPDATE falukant_data.relationship SET marriage_pregnancy_due_at = NULL WHERE id = $1::int; "#; /// Hängengebliebene Ehe-Schwangerschaft (Geburt fehlgeschlagen / inkonsistent): Flag nach 30 Tagen löschen. pub const QUERY_CLEAR_STALE_MARRIAGE_PREGNANCY_DUE: &str = r#" UPDATE falukant_data.relationship SET marriage_pregnancy_due_at = NULL WHERE marriage_pregnancy_due_at IS NOT NULL AND marriage_pregnancy_due_at < NOW() - INTERVAL '30 days'; "#; /// Täglicher Konzeptionswurf (Ehe): bei Treffer wird `marriage_pregnancy_due_at` auf +5 Tage gesetzt. pub const QUERY_TRY_MARRIAGE_CONCEPTION_UPDATE: &str = r#" WITH paired AS ( SELECT r.id AS relationship_id, CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END AS father_cid, CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END AS mother_cid, CASE WHEN c1.gender = 'male' THEN c1.title_of_nobility ELSE c2.title_of_nobility END AS title_of_nobility, CASE WHEN c1.gender = 'male' THEN c1.last_name ELSE c2.last_name END AS last_name, CASE WHEN c1.gender = 'male' THEN c1.region_id ELSE c2.region_id END AS region_id, CASE WHEN c1.gender = 'male' THEN fu1.id ELSE fu2.id END AS father_uid, CASE WHEN c1.gender = 'female' THEN fu1.id ELSE fu2.id END AS mother_uid, (CURRENT_DATE - c_female.birthdate::date)::int AS mother_age_days, ( SELECT COUNT(*)::int FROM falukant_data.child_relation cr WHERE cr.father_character_id = (CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END) AND cr.mother_character_id = (CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END) ) AS shared_children_count FROM falukant_data.relationship r JOIN falukant_type.relationship r2 ON r2.id = r.relationship_type_id AND r2.tr = 'married' JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id JOIN falukant_data.character c_female ON c_female.id = ( CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END ) LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id WHERE r.marriage_pregnancy_due_at IS NULL AND ((c1.gender = 'male' AND c2.gender = 'female') OR (c1.gender = 'female' AND c2.gender = 'male')) AND c_female.pregnancy_due_at IS NULL ), mother_age AS ( SELECT p.relationship_id, p.father_cid, p.mother_cid, p.title_of_nobility, p.last_name, p.region_id, p.father_uid, p.mother_uid, p.mother_age_days, p.shared_children_count, CASE WHEN p.mother_age_days < 4380 THEN 0.005 WHEN p.mother_age_days < 4745 THEN 0.30 WHEN p.mother_age_days < 5110 THEN 0.45 WHEN p.mother_age_days < 5475 THEN 0.55 WHEN p.mother_age_days < 5840 THEN 0.60 WHEN p.mother_age_days < 6205 THEN 0.725 WHEN p.mother_age_days < 6570 THEN 0.80 WHEN p.mother_age_days < 7305 THEN 0.855 WHEN p.mother_age_days < 9125 THEN 0.875 WHEN p.mother_age_days < 10950 THEN 0.84 WHEN p.mother_age_days < 11315 THEN 0.785 WHEN p.mother_age_days < 11680 THEN 0.765 WHEN p.mother_age_days < 12045 THEN 0.74 WHEN p.mother_age_days < 12410 THEN 0.72 WHEN p.mother_age_days < 12775 THEN 0.695 WHEN p.mother_age_days < 13140 THEN 0.65 WHEN p.mother_age_days < 13505 THEN 0.63 WHEN p.mother_age_days < 13870 THEN 0.60 WHEN p.mother_age_days < 14235 THEN 0.55 WHEN p.mother_age_days < 14600 THEN 0.50 WHEN p.mother_age_days < 14965 THEN 0.45 WHEN p.mother_age_days < 15330 THEN 0.35 WHEN p.mother_age_days < 15695 THEN 0.25 WHEN p.mother_age_days < 16060 THEN 0.15 WHEN p.mother_age_days < 16425 THEN 0.075 WHEN p.mother_age_days < 16790 THEN 0.03 WHEN p.mother_age_days < 17155 THEN 0.02 WHEN p.mother_age_days < 17520 THEN 0.015 WHEN p.mother_age_days < 18250 THEN 0.005 ELSE 0.001 END AS prob_year_raw FROM paired p ), mother_age_final AS ( SELECT ma.*, CASE WHEN ma.shared_children_count = 0 AND ma.mother_age_days >= 6570 AND ma.mother_age_days < 16000 THEN GREATEST(ma.prob_year_raw, 1.0) ELSE ma.prob_year_raw END AS prob_year FROM mother_age ma ), conceivable AS ( SELECT ma.relationship_id FROM mother_age_final ma WHERE ma.mother_age_days >= 4380 AND ma.mother_age_days < 18993 AND random() < ma.prob_year ) UPDATE falukant_data.relationship r SET marriage_pregnancy_due_at = NOW() + INTERVAL '5 days' FROM conceivable c WHERE r.id = c.relationship_id; "#; pub const QUERY_MARRIAGE_PREGNANCY_COLUMN_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'relationship' AND column_name = 'marriage_pregnancy_due_at' ) AS ready; "#; /// Ehe: ohne Migration 008 — täglicher Wurf legt sofort ein Kind an (Daemon ruft 1×/Kalendertag auf). pub const QUERY_GET_LEGACY_MARRIAGE_INSTANT_PREGNANCY_CANDIDATES: &str = r#" WITH paired AS ( SELECT CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END AS father_cid, CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END AS mother_cid, CASE WHEN c1.gender = 'male' THEN c1.title_of_nobility ELSE c2.title_of_nobility END AS title_of_nobility, CASE WHEN c1.gender = 'male' THEN c1.last_name ELSE c2.last_name END AS last_name, CASE WHEN c1.gender = 'male' THEN c1.region_id ELSE c2.region_id END AS region_id, CASE WHEN c1.gender = 'male' THEN fu1.id ELSE fu2.id END AS father_uid, CASE WHEN c1.gender = 'female' THEN fu1.id ELSE fu2.id END AS mother_uid, (CURRENT_DATE - c_female.birthdate::date)::int AS mother_age_days, ( SELECT COUNT(*)::int FROM falukant_data.child_relation cr WHERE cr.father_character_id = (CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END) AND cr.mother_character_id = (CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END) ) AS shared_children_count FROM falukant_data.relationship r JOIN falukant_type.relationship r2 ON r2.id = r.relationship_type_id AND r2.tr = 'married' JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id JOIN falukant_data.character c_female ON c_female.id = ( CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END ) LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id WHERE ((c1.gender = 'male' AND c2.gender = 'female') OR (c1.gender = 'female' AND c2.gender = 'male')) AND c_female.pregnancy_due_at IS NULL ), mother_age AS ( SELECT p.father_cid, p.mother_cid, p.title_of_nobility, p.last_name, p.region_id, p.father_uid, p.mother_uid, p.mother_age_days, p.shared_children_count, CASE WHEN p.mother_age_days < 4380 THEN 0.005 WHEN p.mother_age_days < 4745 THEN 0.30 WHEN p.mother_age_days < 5110 THEN 0.45 WHEN p.mother_age_days < 5475 THEN 0.55 WHEN p.mother_age_days < 5840 THEN 0.60 WHEN p.mother_age_days < 6205 THEN 0.725 WHEN p.mother_age_days < 6570 THEN 0.80 WHEN p.mother_age_days < 7305 THEN 0.855 WHEN p.mother_age_days < 9125 THEN 0.875 WHEN p.mother_age_days < 10950 THEN 0.84 WHEN p.mother_age_days < 11315 THEN 0.785 WHEN p.mother_age_days < 11680 THEN 0.765 WHEN p.mother_age_days < 12045 THEN 0.74 WHEN p.mother_age_days < 12410 THEN 0.72 WHEN p.mother_age_days < 12775 THEN 0.695 WHEN p.mother_age_days < 13140 THEN 0.65 WHEN p.mother_age_days < 13505 THEN 0.63 WHEN p.mother_age_days < 13870 THEN 0.60 WHEN p.mother_age_days < 14235 THEN 0.55 WHEN p.mother_age_days < 14600 THEN 0.50 WHEN p.mother_age_days < 14965 THEN 0.45 WHEN p.mother_age_days < 15330 THEN 0.35 WHEN p.mother_age_days < 15695 THEN 0.25 WHEN p.mother_age_days < 16060 THEN 0.15 WHEN p.mother_age_days < 16425 THEN 0.075 WHEN p.mother_age_days < 16790 THEN 0.03 WHEN p.mother_age_days < 17155 THEN 0.02 WHEN p.mother_age_days < 17520 THEN 0.015 WHEN p.mother_age_days < 18250 THEN 0.005 ELSE 0.001 END AS prob_year_raw FROM paired p ), mother_age_final AS ( SELECT ma.*, CASE WHEN ma.shared_children_count = 0 AND ma.mother_age_days >= 6570 AND ma.mother_age_days < 16000 THEN GREATEST(ma.prob_year_raw, 1.0) ELSE ma.prob_year_raw END AS prob_year FROM mother_age ma ) SELECT father_cid, mother_cid, title_of_nobility, last_name, region_id, father_uid, mother_uid, mother_age_days, prob_year * 100.0 AS prob_pct FROM mother_age_final WHERE mother_age_days >= 4380 AND mother_age_days < 18993 AND random() < prob_year; "#; /// Spalten `pregnancy_due_at` / `pregnancy_father_character_id` auf `character` (Migration `011_…`). pub const QUERY_CHARACTER_PLANNED_PREGNANCY_COLUMNS_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'character' AND column_name = 'pregnancy_due_at' ) AS ready; "#; /// Fällige **geplante** Geburten (Weg A): Mutter trägt Termin + Vater; kein Ehe-`relationship`-Bezug. /// Vater NULL oder = Mutter: keine Zeile (Daemon überspringt; optional im Log). pub const QUERY_GET_PLANNED_CHARACTER_BIRTH_DELIVERIES: &str = r#" SELECT c_m.id AS mother_cid, c_f.id AS father_cid, CASE WHEN c_f.gender = 'male' THEN c_f.title_of_nobility ELSE c_m.title_of_nobility END AS title_of_nobility, CASE WHEN c_f.gender = 'male' THEN c_f.last_name ELSE c_m.last_name END AS last_name, CASE WHEN c_f.gender = 'male' THEN c_f.region_id ELSE c_m.region_id END AS region_id, fu_f.id AS father_uid, fu_m.id AS mother_uid, CASE WHEN EXISTS ( SELECT 1 FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' WHERE (r.character1_id = c_f.id AND r.character2_id = c_m.id) OR (r.character1_id = c_m.id AND r.character2_id = c_f.id) ) THEN 'lover' ELSE 'marriage' END AS birth_context FROM falukant_data.character c_m JOIN falukant_data.character c_f ON c_f.id = c_m.pregnancy_father_character_id LEFT JOIN falukant_data.falukant_user fu_m ON fu_m.id = c_m.user_id LEFT JOIN falukant_data.falukant_user fu_f ON fu_f.id = c_f.user_id WHERE c_m.pregnancy_due_at IS NOT NULL AND c_m.pregnancy_due_at <= NOW() AND c_m.pregnancy_father_character_id IS NOT NULL AND c_m.pregnancy_father_character_id <> c_m.id AND c_m.health > 0; "#; pub const QUERY_CLEAR_CHARACTER_PREGNANCY_AFTER_BIRTH: &str = r#" UPDATE falukant_data.character SET pregnancy_due_at = NULL, pregnancy_father_character_id = NULL, updated_at = NOW() WHERE id = $1::int; "#; /// Wie Node-Admin: `birth_context` / `legitimacy` / `public_known` je nach Liebschaft vs. Ehe. pub const QUERY_INSERT_CHILD_RELATION_PLANNED_BIRTH: &str = r#" INSERT INTO falukant_data.child_relation ( father_character_id, mother_character_id, child_character_id, name_set, legitimacy, birth_context, public_known, created_at, updated_at ) VALUES ( $1::int, $2::int, $3::int, FALSE, CASE WHEN $4::text = 'lover' THEN 'hidden_bastard'::varchar ELSE 'legitimate'::varchar END, CASE WHEN $4::text = 'lover' THEN 'lover'::varchar ELSE 'marriage'::varchar END, CASE WHEN $4::text = 'lover' THEN FALSE ELSE TRUE END, NOW(), NOW() ); "#; pub const QUERY_INSERT_CHILD: &str = r#" INSERT INTO falukant_data.character ( user_id, region_id, first_name, last_name, birthdate, gender, title_of_nobility, mood_id, created_at, updated_at ) VALUES ( NULL, $1::int, ( SELECT id FROM falukant_predefine.firstname WHERE gender = $2 ORDER BY RANDOM() LIMIT 1 ), $3::int, NOW(), $2::varchar, $4::int, ( SELECT id FROM falukant_type.mood ORDER BY RANDOM() LIMIT 1 ), NOW(), NOW() ) RETURNING id AS child_cid; "#; pub const QUERY_INSERT_CHILD_RELATION: &str = r#" INSERT INTO falukant_data.child_relation ( father_character_id, mother_character_id, child_character_id, name_set, created_at, updated_at ) VALUES ( $1::int, $2::int, $3::int, FALSE, NOW(), NOW() ); "#; pub const QUERY_DELETE_KNOWLEDGE: &str = r#" DELETE FROM falukant_data.knowledge WHERE character_id = $1; "#; pub const QUERY_DELETE_DEBTORS_PRISM: &str = r#" DELETE FROM falukant_data.debtors_prism WHERE character_id = $1; "#; pub const QUERY_DELETE_POLITICAL_OFFICE: &str = r#" WITH removed AS ( DELETE FROM falukant_data.political_office WHERE character_id = $1 RETURNING office_type_id, region_id ), affected AS ( SELECT DISTINCT office_type_id, region_id FROM removed ), seats AS ( SELECT pot.id AS office_type_id, rt.id AS region_id, pot.seats_per_region AS seats_total FROM falukant_type.political_office_type AS pot JOIN falukant_type.region AS rt ON pot.region_type = rt.label_tr JOIN affected AS a ON a.office_type_id = pot.id AND a.region_id = rt.id ), ranked AS ( SELECT po.id, po.office_type_id, po.region_id, s.seats_total, ROW_NUMBER() OVER ( PARTITION BY po.office_type_id, po.region_id ORDER BY po.created_at DESC ) AS rn FROM falukant_data.political_office AS po JOIN seats AS s ON s.office_type_id = po.office_type_id AND s.region_id = po.region_id ), to_delete AS ( SELECT id FROM ranked WHERE rn > seats_total ) DELETE FROM falukant_data.political_office WHERE id IN (SELECT id FROM to_delete); "#; pub const QUERY_DELETE_ELECTION_CANDIDATE: &str = r#" DELETE FROM falukant_data.election_candidate WHERE character_id = $1; "#; pub const QUERY_GET_STOCK_TYPE_ID: &str = r#" SELECT id FROM falukant_type.stock WHERE label_tr = $1 LIMIT 1; "#; pub const QUERY_GET_INVENTORY_ITEMS: &str = r#" SELECT i.id AS inventory_id, i.quantity AS inventory_quantity, i.stock_id FROM falukant_data.inventory i JOIN falukant_data.stock s ON i.stock_id = s.id JOIN falukant_data.branch b ON s.branch_id = b.id WHERE b.region_id = $1 AND s.stock_type_id = $2; "#; pub const QUERY_REDUCE_INVENTORY: &str = r#" UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; "#; pub const QUERY_DELETE_INVENTORY: &str = r#" DELETE FROM falukant_data.inventory WHERE stock_id = $1; "#; pub const QUERY_DELETE_STOCK: &str = r#" DELETE FROM falukant_data.stock WHERE id = $1; "#; pub const QUERY_GET_STOCK_INVENTORY: &str = r#" SELECT id, quantity FROM falukant_data.inventory WHERE stock_id = $1; "#; pub const QUERY_CAP_INVENTORY: &str = r#" UPDATE falukant_data.inventory SET quantity = $1 WHERE id = $2; "#; pub const QUERY_GET_USER_INVENTORY_ITEMS: &str = r#" SELECT i.id AS inventory_id, i.quantity AS inventory_quantity, i.stock_id FROM falukant_data.inventory i JOIN falukant_data.stock s ON i.stock_id = s.id JOIN falukant_data.branch b ON s.branch_id = b.id WHERE b.falukant_user_id = $1 AND s.stock_type_id = $2; "#; // Produce worker queries pub const QUERY_GET_FINISHED_PRODUCTIONS: &str = r#" SELECT DISTINCT ON (p.id) p.id AS production_id, p.branch_id, p.product_id, p.quantity, p.start_timestamp, pr.production_time, br.region_id, br.falukant_user_id AS user_id, ROUND( GREATEST( 0, LEAST( 100, ( (COALESCE(k.knowledge, 0) * 0.75 + COALESCE(k2.knowledge, 0) * 0.25) * COALESCE(pwe.quality_effect, 100) / 100.0 ) ) ) )::int AS quality FROM falukant_data.production p JOIN falukant_type.product pr ON p.product_id = pr.id JOIN falukant_data.branch br ON p.branch_id = br.id LEFT JOIN falukant_data.character c ON c.user_id = br.falukant_user_id LEFT JOIN falukant_data.knowledge k ON p.product_id = k.product_id AND k.character_id = c.id LEFT JOIN falukant_data.director d ON d.employer_user_id = br.falukant_user_id LEFT JOIN falukant_data.knowledge k2 ON k2.character_id = d.director_character_id AND k2.product_id = p.product_id LEFT JOIN falukant_data.weather w ON w.region_id = br.region_id LEFT JOIN falukant_type.product_weather_effect pwe ON pwe.product_id = p.product_id AND pwe.weather_type_id = w.weather_type_id -- Prüfe, ob genug freier Lagerplatz vorhanden ist (korrelierte Subqueries, keine JOIN-Multiplikation) LEFT JOIN ( SELECT br2.id AS branch_id, (SELECT COALESCE(SUM(quantity), 0) FROM falukant_data.stock WHERE branch_id = br2.id) AS stock_size, (SELECT COALESCE(SUM(fdi.quantity), 0) FROM falukant_data.stock fds JOIN falukant_data.inventory fdi ON fdi.stock_id = fds.id WHERE fds.branch_id = br2.id) AS used_in_stock, (SELECT COALESCE(SUM(quantity), 0) FROM falukant_data.production WHERE branch_id = br2.id) AS running_productions_quantity FROM falukant_data.branch br2 ) capacity ON capacity.branch_id = p.branch_id -- Wetter-Effekte derzeit aus der Qualitätsberechnung entfernt WHERE p.start_timestamp + INTERVAL '1 minute' * pr.production_time <= NOW() -- Freier Platz = stock_size - used_in_stock; laufende Produktionen belegen noch keinen Platz. -- Es muss genug Platz für p.quantity sein: (stock_size - used_in_stock) >= p.quantity AND (capacity.stock_size - capacity.used_in_stock) >= p.quantity ORDER BY p.id, p.start_timestamp; "#; pub const QUERY_DELETE_PRODUCTION: &str = r#" DELETE FROM falukant_data.production WHERE id = $1; "#; pub const QUERY_INSERT_UPDATE_PRODUCTION_LOG: &str = r#" INSERT INTO falukant_log.production ( region_id, product_id, quantity, producer_id, production_date ) VALUES ($1, $2, $3, $4, CURRENT_DATE) ON CONFLICT (producer_id, product_id, region_id, production_date) DO UPDATE SET quantity = falukant_log.production.quantity + EXCLUDED.quantity; "#; pub const QUERY_ADD_OVERPRODUCTION_NOTIFICATION: &str = r#" INSERT INTO falukant_log.notification ( user_id, tr, shown, created_at, updated_at ) VALUES ($1, $2, FALSE, NOW(), NOW()); "#; pub const QUERY_UPDATE_OVERPRODUCTION_NOTIFICATION: &str = r#" UPDATE falukant_log.notification SET tr = jsonb_set( tr::jsonb, '{value}', to_jsonb(COALESCE((tr::jsonb->>'value')::int, 0) + $3) )::text, updated_at = NOW() WHERE user_id = $1 AND shown = FALSE AND tr::text LIKE '%"tr":"production.overproduction"%' AND (tr::jsonb->>'branch_id')::int = $2; "#; pub const QUERY_FIND_OVERPRODUCTION_NOTIFICATION: &str = r#" SELECT id, tr FROM falukant_log.notification WHERE user_id = $1 AND shown = FALSE AND tr::text LIKE '%"tr":"production.overproduction"%' AND (tr::jsonb->>'branch_id')::int = $2 ORDER BY created_at DESC LIMIT 1; "#; // Aliases for personal variants (keeps original prepared statement names used in events.worker) pub const QUERY_REDUCE_INVENTORY_PERSONAL: &str = QUERY_REDUCE_INVENTORY; pub const QUERY_DELETE_INVENTORY_PERSONAL: &str = QUERY_DELETE_INVENTORY; pub const QUERY_DELETE_STOCK_PERSONAL: &str = QUERY_DELETE_STOCK; pub const QUERY_GET_STOCK_INVENTORY_PERSONAL: &str = QUERY_GET_STOCK_INVENTORY; pub const QUERY_CAP_INVENTORY_PERSONAL: &str = QUERY_CAP_INVENTORY; // value_recalculation worker queries pub const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#" UPDATE falukant_data.knowledge k SET knowledge = LEAST(100, k.knowledge + 1) FROM falukant_data.character c JOIN falukant_log.production p ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' WHERE c.id = k.character_id AND c.user_id = 18 AND k.product_id = 10; "#; pub const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#" DELETE FROM falukant_log.production flp WHERE DATE(flp.production_timestamp) < CURRENT_DATE; "#; pub const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#" SELECT p.producer_id FROM falukant_log.production p WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' GROUP BY producer_id; "#; pub const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#" UPDATE falukant_data.town_product_worth tpw SET worth_percent = GREATEST( 0, LEAST( CASE WHEN s.quantity > COALESCE(s.avg_sells, 0) * 1.05 THEN tpw.worth_percent + 1 WHEN s.quantity < COALESCE(s.avg_sells, 0) * 0.95 THEN tpw.worth_percent - 1 ELSE tpw.worth_percent END, 100 ) ) FROM ( SELECT region_id, product_id, quantity, (SELECT AVG(quantity) FROM falukant_log.sell avs WHERE avs.product_id = s.product_id) AS avg_sells FROM falukant_log.sell s WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' ) s WHERE tpw.region_id = s.region_id AND tpw.product_id = s.product_id; "#; pub const QUERY_DELETE_REGION_SELL_PRICE: &str = r#" DELETE FROM falukant_log.sell s WHERE DATE(s.sell_timestamp) < CURRENT_DATE; "#; pub const QUERY_GET_SELL_REGIONS: &str = r#" SELECT s.region_id FROM falukant_log.sell s WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' GROUP BY region_id; "#; pub const QUERY_HOURLY_PRICE_RECALCULATION: &str = r#" WITH city_sales AS ( SELECT s.region_id, s.product_id, SUM(s.quantity) AS total_sold FROM falukant_log.sell s WHERE s.sell_timestamp >= NOW() - INTERVAL '1 hour' GROUP BY s.region_id, s.product_id ), world_avg AS ( SELECT product_id, AVG(total_sold) AS avg_sold FROM city_sales GROUP BY product_id ), adjustments AS ( SELECT cs.region_id, cs.product_id, CASE WHEN cs.total_sold > COALESCE(wa.avg_sold, 0) * 1.05 THEN 1.10 WHEN cs.total_sold < COALESCE(wa.avg_sold, 0) * 0.95 THEN 0.90 ELSE 1.00 END AS factor FROM city_sales cs JOIN world_avg wa ON wa.product_id = cs.product_id ) UPDATE falukant_data.town_product_worth tpw SET worth_percent = GREATEST(0, LEAST(100, tpw.worth_percent * adj.factor)) FROM adjustments adj WHERE tpw.region_id = adj.region_id AND tpw.product_id = adj.product_id; "#; /// Fügt für eine Region Preise in die Historie ein. /// Berechnet den regionalen Produktpreis als sell_cost * worth_percent / 100. pub const QUERY_INSERT_PRODUCT_PRICE_HISTORY: &str = r#" INSERT INTO falukant_log.product_price_history (product_id, region_id, price, recorded_at) SELECT tpw.product_id, tpw.region_id, ROUND(ftp.sell_cost * tpw.worth_percent / 100.0, 2) AS price, NOW() AS recorded_at FROM falukant_data.town_product_worth tpw JOIN falukant_type.product ftp ON ftp.id = tpw.product_id WHERE tpw.region_id = $1; "#; pub const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#" WITH updated_relations AS ( UPDATE falukant_data.relationship AS rel SET relationship_type_id = ( SELECT id FROM falukant_type.relationship AS rt WHERE rt.tr = 'married' ), marriage_satisfaction = 55, marriage_drift_high = 0, marriage_drift_low = 0 WHERE rel.id IN ( SELECT rel2.id FROM falukant_data.party AS p JOIN falukant_type.party AS pt ON pt.id = p.party_type_id AND pt.tr = 'wedding' JOIN falukant_data.falukant_user AS fu ON fu.id = p.falukant_user_id JOIN falukant_data.character AS c ON c.user_id = fu.id JOIN falukant_data.relationship AS rel2 ON rel2.character1_id = c.id OR rel2.character2_id = c.id JOIN falukant_type.relationship AS rt2 ON rt2.id = rel2.relationship_type_id AND rt2.tr = 'engaged' WHERE p.created_at <= NOW() - INTERVAL '1 day' ) RETURNING character1_id, character2_id ) SELECT c1.user_id AS character1_user, c2.user_id AS character2_user FROM updated_relations AS ur JOIN falukant_data.character AS c1 ON c1.id = ur.character1_id JOIN falukant_data.character AS c2 ON c2.id = ur.character2_id; "#; pub const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#" SELECT l.id, l.associated_falukant_user_id, l.associated_learning_character_id, l.learn_all_products, l.learning_recipient_id, l.product_id, lr.tr FROM falukant_data.learning l JOIN falukant_type.learn_recipient lr ON lr.id = l.learning_recipient_id WHERE l.learning_is_executed = FALSE AND l.created_at + INTERVAL '1 day' < NOW(); "#; pub const QUERY_GET_OWN_CHARACTER_ID: &str = r#" SELECT id FROM falukant_data.character c WHERE c.user_id = $1; "#; pub const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#" UPDATE falukant_data.knowledge k SET knowledge = LEAST(100, k.knowledge + $1) WHERE k.character_id = $2 AND k.product_id = $3; "#; pub const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#" UPDATE falukant_data.knowledge k SET knowledge = LEAST(100, k.knowledge + $1) WHERE k.character_id = $2; "#; pub const QUERY_SET_LEARNING_DONE: &str = r#" UPDATE falukant_data.learning SET learning_is_executed = TRUE, updated_at = NOW() WHERE id = $1; "#; // Church Office Queries (siehe docs/FALUKANT_CHURCH_DAEMON.md) pub const QUERY_FIND_AVAILABLE_CHURCH_OFFICES: &str = r#" SELECT cot.id AS office_type_id, cot.name AS office_type_name, cot.hierarchy_level, cot.seats_per_region, cot.region_type, r.id AS region_id, COUNT(co.id) AS occupied_seats FROM falukant_type.church_office_type cot CROSS JOIN falukant_data.region r JOIN falukant_type.region tr ON r.region_type_id = tr.id LEFT JOIN falukant_data.church_office co ON cot.id = co.office_type_id AND co.region_id = r.id WHERE tr.label_tr = cot.region_type GROUP BY cot.id, cot.name, cot.hierarchy_level, cot.seats_per_region, cot.region_type, r.id HAVING COUNT(co.id) < cot.seats_per_region ORDER BY cot.hierarchy_level ASC, r.id; "#; pub const QUERY_FIND_CHURCH_SUPERVISOR: &str = r#" SELECT co.id AS office_id, co.character_id AS supervisor_character_id, co.region_id, cot.hierarchy_level FROM falukant_data.church_office co JOIN falukant_type.church_office_type cot ON co.office_type_id = cot.id WHERE co.region_id = $1 AND cot.hierarchy_level > ( SELECT hierarchy_level FROM falukant_type.church_office_type WHERE id = $2 ) ORDER BY cot.hierarchy_level ASC LIMIT 1; "#; pub const QUERY_GET_CHURCH_OFFICE_REQUIREMENTS: &str = r#" SELECT id, office_type_id, prerequisite_office_type_id, min_title_level FROM falukant_predefine.church_office_requirement WHERE office_type_id = $1; "#; /// Optional für Backend/API; der Daemon nutzt `QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING`. #[allow(dead_code)] pub const QUERY_GET_PENDING_CHURCH_APPLICATIONS: &str = r#" SELECT ca.id AS application_id, ca.office_type_id, ca.character_id AS applicant_character_id, ca.region_id, ca.supervisor_id, cot.name AS office_type_name, cot.hierarchy_level FROM falukant_data.church_application ca JOIN falukant_type.church_office_type cot ON ca.office_type_id = cot.id WHERE ca.status = 'pending' AND ca.supervisor_id = $1 ORDER BY cot.hierarchy_level ASC, ca.created_at ASC; "#; /// Voraussetzung: Migration `007_falukant_character_church_career.sql` (highest_church_hierarchy_ever). pub const QUERY_CHECK_CHARACTER_ELIGIBILITY: &str = r#" WITH prereq AS ( SELECT $2::int AS prereq_type_id, CASE WHEN $2::int IS NULL THEN NULL ELSE ( SELECT hierarchy_level FROM falukant_type.church_office_type WHERE id = $2::int ) END AS prereq_hl ), char_h AS ( SELECT c.id AS character_id, c.title_of_nobility, t.level AS title_level, EXISTS( SELECT 1 FROM falukant_data.church_office co2 WHERE co2.character_id = c.id ) AS has_office, COALESCE(c.highest_church_hierarchy_ever, 0)::int AS highest_ever, COALESCE(( SELECT MAX(cot2.hierarchy_level) FROM falukant_data.church_office co2 JOIN falukant_type.church_office_type cot2 ON co2.office_type_id = cot2.id WHERE co2.character_id = c.id ), 0) AS current_max_hl FROM falukant_data.character c LEFT JOIN falukant_type.title t ON c.title_of_nobility = t.id WHERE c.id = $1 ) SELECT ch.character_id, ch.title_level, ch.has_office, CASE WHEN pr.prereq_type_id IS NULL THEN TRUE ELSE ( EXISTS( SELECT 1 FROM falukant_data.church_office co WHERE co.character_id = $1 AND co.office_type_id = pr.prereq_type_id ) OR ( pr.prereq_hl IS NOT NULL AND GREATEST(ch.highest_ever, ch.current_max_hl) >= pr.prereq_hl ) ) END AS has_prerequisite, CASE WHEN $3::int IS NULL THEN TRUE ELSE COALESCE(ch.title_level, 0) >= $3::int END AS meets_title_requirement FROM char_h ch CROSS JOIN prereq pr; "#; pub const QUERY_APPROVE_CHURCH_APPLICATION: &str = r#" WITH updated_application AS ( UPDATE falukant_data.church_application SET status = 'approved', decision_date = NOW(), updated_at = NOW() WHERE id = $1 AND status = 'pending' RETURNING office_type_id, character_id, region_id, supervisor_id ), inserted_office AS ( INSERT INTO falukant_data.church_office (office_type_id, character_id, region_id, supervisor_id, created_at, updated_at) SELECT office_type_id, character_id, region_id, supervisor_id, NOW(), NOW() FROM updated_application WHERE NOT EXISTS( SELECT 1 FROM falukant_data.church_office co WHERE co.office_type_id = updated_application.office_type_id AND co.region_id = updated_application.region_id AND co.character_id = updated_application.character_id ) RETURNING id, office_type_id, character_id, region_id ), upd_highest AS ( UPDATE falukant_data.character c SET highest_church_hierarchy_ever = GREATEST( COALESCE(c.highest_church_hierarchy_ever, 0), io.hl )::smallint FROM ( SELECT io2.character_id, cot.hierarchy_level AS hl FROM inserted_office io2 JOIN falukant_type.church_office_type cot ON cot.id = io2.office_type_id ) io WHERE c.id = io.character_id RETURNING c.id ), remove_lower_ranked AS ( DELETE FROM falukant_data.church_office co WHERE co.id IN ( SELECT co3.id FROM falukant_data.church_office co3 JOIN falukant_type.church_office_type cot ON co3.office_type_id = cot.id WHERE co3.character_id IN (SELECT character_id FROM inserted_office) AND EXISTS ( SELECT 1 FROM falukant_data.church_office co2 JOIN falukant_type.church_office_type cot2 ON co2.office_type_id = cot2.id WHERE co2.character_id = co3.character_id AND cot2.hierarchy_level > cot.hierarchy_level ) ) ) SELECT id AS office_id, office_type_id, character_id, region_id FROM inserted_office; "#; pub const QUERY_REJECT_CHURCH_APPLICATION: &str = r#" UPDATE falukant_data.church_application SET status = 'rejected', decision_date = NOW(), updated_at = NOW() WHERE id = $1 AND status = 'pending' RETURNING id; "#; /// Nur NPC-Vorgesetzte: Spieler-Entscheidungen nicht per Timeout überschreiben. pub const QUERY_GET_OLD_PENDING_CHURCH_APPLICATIONS: &str = r#" SELECT ca.id AS application_id, ca.office_type_id, ca.character_id, ca.region_id, ca.supervisor_id FROM falukant_data.church_application ca JOIN falukant_data.character sup ON sup.id = ca.supervisor_id WHERE ca.status = 'pending' AND ca.created_at <= NOW() - INTERVAL '36 hours' AND sup.user_id IS NULL ORDER BY ca.created_at ASC; "#; pub const QUERY_AUTO_APPROVE_CHURCH_APPLICATION: &str = r#" WITH updated_application AS ( UPDATE falukant_data.church_application SET status = 'approved', decision_date = NOW(), updated_at = NOW() WHERE id = $1 AND status = 'pending' AND created_at <= NOW() - INTERVAL '36 hours' RETURNING office_type_id, character_id, region_id, supervisor_id ), inserted_office AS ( INSERT INTO falukant_data.church_office (office_type_id, character_id, region_id, supervisor_id, created_at, updated_at) SELECT office_type_id, character_id, region_id, supervisor_id, NOW(), NOW() FROM updated_application WHERE NOT EXISTS( SELECT 1 FROM falukant_data.church_office co WHERE co.office_type_id = updated_application.office_type_id AND co.region_id = updated_application.region_id AND co.character_id = updated_application.character_id ) RETURNING id, office_type_id, character_id, region_id ), upd_highest AS ( UPDATE falukant_data.character c SET highest_church_hierarchy_ever = GREATEST( COALESCE(c.highest_church_hierarchy_ever, 0), io.hl )::smallint FROM ( SELECT io2.character_id, cot.hierarchy_level AS hl FROM inserted_office io2 JOIN falukant_type.church_office_type cot ON cot.id = io2.office_type_id ) io WHERE c.id = io.character_id RETURNING c.id ), remove_lower_ranked AS ( DELETE FROM falukant_data.church_office co WHERE co.id IN ( SELECT co3.id FROM falukant_data.church_office co3 JOIN falukant_type.church_office_type cot ON co3.office_type_id = cot.id WHERE co3.character_id IN (SELECT character_id FROM inserted_office) AND EXISTS ( SELECT 1 FROM falukant_data.church_office co2 JOIN falukant_type.church_office_type cot2 ON co2.office_type_id = cot2.id WHERE co2.character_id = co3.character_id AND cot2.hierarchy_level > cot.hierarchy_level ) ) ) SELECT id AS office_id, office_type_id, character_id, region_id FROM inserted_office; "#; pub const QUERY_CREATE_CHURCH_APPLICATION_JOB: &str = r#" INSERT INTO falukant_data.church_application (office_type_id, character_id, region_id, supervisor_id, status, created_at, updated_at) SELECT $1::int AS office_type_id, $2::int AS character_id, $3::int AS region_id, $4::int AS supervisor_id, 'pending' AS status, NOW() AS created_at, NOW() AS updated_at WHERE NOT EXISTS( SELECT 1 FROM falukant_data.church_application ca WHERE ca.office_type_id = $1::int AND ca.character_id = $2::int AND ca.region_id = $3::int AND ca.status = 'pending' ) RETURNING id; "#; /// Nur NPCs: Spielerbewerbungen laufen über die UI. pub const QUERY_GET_CHARACTERS_FOR_CHURCH_OFFICE: &str = r#" SELECT DISTINCT c.id AS character_id, c.user_id, c.region_id, c.title_of_nobility, t.level AS title_level FROM falukant_data.character c LEFT JOIN falukant_type.title t ON c.title_of_nobility = t.id WHERE c.region_id = $1 AND c.health > 0 AND c.user_id IS NULL AND NOT EXISTS( SELECT 1 FROM falukant_data.church_office co WHERE co.character_id = c.id ) ORDER BY RANDOM() LIMIT $2; "#; pub const QUERY_COUNT_PENDING_CHURCH_APPS_BY_OFFICE_REGION: &str = r#" SELECT COUNT(*)::int AS cnt FROM falukant_data.church_application ca WHERE ca.office_type_id = $1::int AND ca.region_id = $2::int AND ca.status = 'pending'; "#; pub const QUERY_GET_CHURCH_OFFICE_OCCUPIED_COUNT: &str = r#" SELECT COUNT(*)::int AS cnt FROM falukant_data.church_office co WHERE co.office_type_id = $1::int AND co.region_id = $2::int; "#; pub const QUERY_IS_CHARACTER_NPC: &str = r#" SELECT (c.user_id IS NULL) AS is_npc FROM falukant_data.character c WHERE c.id = $1::int; "#; pub const QUERY_GET_PENDING_CHURCH_APPLICATIONS_FOR_SCORING: &str = r#" SELECT ca.id AS application_id, ca.office_type_id, ca.character_id AS applicant_character_id, ca.region_id, ca.created_at, cot.hierarchy_level AS office_hierarchy_level, cot.seats_per_region, COALESCE(sc.reputation, 50)::float8 AS supervisor_reputation, COALESCE(ac.reputation, 50)::float8 AS applicant_reputation, COALESCE(ac.highest_church_hierarchy_ever, 0)::int AS applicant_highest_ever, COALESCE(t.level, 0)::int AS applicant_title_level, COALESCE(( SELECT MAX(cot2.hierarchy_level) FROM falukant_data.church_office co2 JOIN falukant_type.church_office_type cot2 ON co2.office_type_id = cot2.id WHERE co2.character_id = ac.id ), 0)::int AS applicant_current_max_hierarchy, (CURRENT_DATE - ac.birthdate::date)::int AS applicant_age_days FROM falukant_data.church_application ca JOIN falukant_data.character ac ON ac.id = ca.character_id JOIN falukant_data.character sc ON sc.id = ca.supervisor_id JOIN falukant_type.church_office_type cot ON cot.id = ca.office_type_id LEFT JOIN falukant_type.title t ON t.id = ac.title_of_nobility WHERE ca.status = 'pending' AND ca.supervisor_id = $1::int ORDER BY ca.created_at ASC; "#; pub const QUERY_INTERIM_APPOINT_CHURCH_OFFICE: &str = r#" INSERT INTO falukant_data.church_office (office_type_id, character_id, region_id, supervisor_id, created_at, updated_at) SELECT $1::int, $2::int, $3::int, NULL, NOW(), NOW() WHERE ( SELECT COUNT(*)::int FROM falukant_data.church_office co WHERE co.office_type_id = $1::int AND co.region_id = $3::int ) < $4::int AND NOT EXISTS ( SELECT 1 FROM falukant_data.church_office co WHERE co.character_id = $2::int AND co.office_type_id = $1::int AND co.region_id = $3::int ) RETURNING id, office_type_id, character_id, region_id; "#; pub const QUERY_UPDATE_CHARACTER_HIGHEST_CHURCH_FROM_OFFICE_TYPE: &str = r#" UPDATE falukant_data.character c SET highest_church_hierarchy_ever = GREATEST( COALESCE(c.highest_church_hierarchy_ever, 0), (SELECT cot.hierarchy_level FROM falukant_type.church_office_type cot WHERE cot.id = $2::int) )::smallint WHERE c.id = $1::int RETURNING c.id; "#; pub const QUERY_FIND_INTERIM_CHURCH_NPC_CANDIDATE: &str = r#" SELECT c.id AS character_id FROM falukant_data.character c WHERE c.region_id = $1::int AND c.user_id IS NULL AND c.health > 0 AND NOT EXISTS ( SELECT 1 FROM falukant_data.church_office co JOIN falukant_type.church_office_type cot ON cot.id = co.office_type_id WHERE co.character_id = c.id AND cot.hierarchy_level >= ( SELECT hierarchy_level FROM falukant_type.church_office_type WHERE id = $2::int ) ) ORDER BY COALESCE(c.reputation, 50) DESC, COALESCE(c.highest_church_hierarchy_ever, 0) DESC LIMIT 1; "#; pub const QUERY_REMOVE_LOWER_CHURCH_OFFICES_FOR_CHARACTER: &str = r#" DELETE FROM falukant_data.church_office co WHERE co.character_id = $1::int AND co.id IN ( SELECT co3.id FROM falukant_data.church_office co3 JOIN falukant_type.church_office_type cot ON co3.office_type_id = cot.id WHERE co3.character_id = $1::int AND EXISTS ( SELECT 1 FROM falukant_data.church_office co2 JOIN falukant_type.church_office_type cot2 ON co2.office_type_id = cot2.id WHERE co2.character_id = co3.character_id AND cot2.hierarchy_level > cot.hierarchy_level ) ); "#; // --- Falukant: Dienerschaft (siehe migrations/004_falukant_servants_daemon.sql) --- pub const QUERY_SERVANTS_SCHEMA_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'user_house' AND column_name = 'servants_last_daily_at' ) AS ready; "#; pub const QUERY_GET_SERVANT_DAILY_ROWS: &str = r#" SELECT DISTINCT ON (uh.id) uh.id AS user_house_id, fu.id AS falukant_user_id, c.id AS character_id, COALESCE(c.reputation, 50)::float8 AS reputation, COALESCE(t.level, 0)::int AS title_level, COALESCE(ht.position, 0)::int AS house_position, COALESCE(ht.cost, 0)::bigint AS house_cost, uh.servant_count, uh.servant_quality, COALESCE(NULLIF(TRIM(uh.servant_pay_level), ''), 'normal') AS servant_pay_level, uh.household_order FROM falukant_data.user_house uh JOIN falukant_data.falukant_user fu ON fu.id = uh.user_id JOIN falukant_data.character c ON c.user_id = fu.id AND c.health > 0 LEFT JOIN falukant_type.title t ON t.id = c.title_of_nobility LEFT JOIN falukant_type.house ht ON ht.id = uh.house_type_id WHERE (uh.servants_last_daily_at IS NULL OR (uh.servants_last_daily_at::date < CURRENT_DATE)) AND NOT EXISTS ( SELECT 1 FROM falukant_type.house h WHERE h.id = uh.house_type_id AND h.label_tr = 'under_bridge' ) ORDER BY uh.id, c.id; "#; pub const QUERY_UPDATE_USER_HOUSE_SERVANT_DAILY: &str = r#" UPDATE falukant_data.user_house SET household_order = $1::smallint, servant_quality = $2::smallint, servant_discretion_modifier = $3::smallint, servants_last_daily_at = NOW() WHERE id = $4::int; "#; pub const QUERY_UPDATE_MARRIAGE_SATISFACTION_ADD_FOR_CHARACTER: &str = r#" UPDATE falukant_data.relationship r SET marriage_satisfaction = GREATEST(0, LEAST(100, marriage_satisfaction + $1::int)) FROM falukant_type.relationship rt WHERE rt.id = r.relationship_type_id AND rt.tr IN ('married', 'engaged', 'wooing') AND (r.character1_id = $2::int OR r.character2_id = $2::int); "#; pub const QUERY_GET_SERVANT_MONTHLY_ROWS: &str = r#" SELECT DISTINCT ON (uh.id) uh.id AS user_house_id, fu.id AS falukant_user_id, c.id AS character_id, COALESCE(c.reputation, 50)::float8 AS reputation, COALESCE(t.level, 0)::int AS title_level, COALESCE(ht.position, 0)::int AS house_position, COALESCE(ht.cost, 0)::bigint AS house_cost, uh.servant_count, uh.servant_quality, COALESCE(NULLIF(TRIM(uh.servant_pay_level), ''), 'normal') AS servant_pay_level, uh.household_order, COALESCE(fu.money, 0)::float8 AS user_money, uh.servants_underfunded FROM falukant_data.user_house uh JOIN falukant_data.falukant_user fu ON fu.id = uh.user_id JOIN falukant_data.character c ON c.user_id = fu.id AND c.health > 0 LEFT JOIN falukant_type.title t ON t.id = c.title_of_nobility LEFT JOIN falukant_type.house ht ON ht.id = uh.house_type_id -- Monatstick (Spielzeit): alle ~2 h, nicht Kalendermonat (siehe docs/FALUKANT_DAEMON_AENDERUNGSNOTIZ_ZEITMASSSTAB.md) WHERE (uh.servants_last_monthly_at IS NULL OR uh.servants_last_monthly_at < NOW() - INTERVAL '2 hours') AND NOT EXISTS ( SELECT 1 FROM falukant_type.house h WHERE h.id = uh.house_type_id AND h.label_tr = 'under_bridge' ) ORDER BY uh.id, c.id; "#; pub const QUERY_COUNT_ACTIVE_LOVERS_FOR_CHARACTER: &str = r#" SELECT COUNT(*)::int AS cnt FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' JOIN falukant_data.relationship_state rs ON rs.relationship_id = r.id AND rs.active = true WHERE r.character1_id = $1::int OR r.character2_id = $1::int; "#; pub const QUERY_UPDATE_USER_HOUSE_SERVANT_MONTHLY_META: &str = r#" UPDATE falukant_data.user_house SET servants_underfunded = $1::boolean, servants_last_monthly_at = NOW() WHERE id = $2::int; "#; pub const QUERY_UPDATE_USER_HOUSE_SERVANT_UNDERFUNDED_PENALTY: &str = r#" UPDATE falukant_data.user_house SET servant_quality = GREATEST(0, servant_quality - 4), household_order = GREATEST(0, household_order - 6), servant_discretion_modifier = GREATEST(-100, LEAST(100, COALESCE(servant_discretion_modifier, 0) + $1::int)) WHERE id = $2::int; "#; // --- Falukant: Familie / Liebhaber / Ehezufriedenheit (siehe migrations/001_falukant_family_lovers.sql) --- pub const QUERY_FAMILY_SCHEMA_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'relationship_state' AND column_name = 'last_daily_processed_at' ) AS ready; "#; pub const QUERY_GET_ACTIVE_LOVER_ROWS_FOR_DAILY: &str = r#" SELECT r.id AS rel_id, r.character1_id AS c1, r.character2_id AS c2, rs.lover_role, rs.affection, rs.visibility, rs.discretion, rs.maintenance_level, rs.status_fit, rs.monthly_base_cost, rs.scandal_extra_daily_pct, rs.months_underfunded, COALESCE(rs.acknowledged, false) AS acknowledged, c1.gender AS g1, c2.gender AS g2, COALESCE(t1.tr, '') AS title1_tr, COALESCE(t2.tr, '') AS title2_tr, COALESCE(c1.reputation, 50)::float8 AS rep1, COALESCE(c2.reputation, 50)::float8 AS rep2, fu1.id AS user1_id, fu2.id AS user2_id, LEAST( ((CURRENT_DATE - c1.birthdate::date) / 365), ((CURRENT_DATE - c2.birthdate::date) / 365) )::int AS min_age_years, COALESCE(( SELECT MAX(uh.servant_discretion_modifier)::int FROM falukant_data.user_house uh WHERE uh.user_id = fu1.id ), 0) AS servant_disc_u1, COALESCE(( SELECT MAX(uh.servant_discretion_modifier)::int FROM falukant_data.user_house uh WHERE uh.user_id = fu2.id ), 0) AS servant_disc_u2 FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' JOIN falukant_data.relationship_state rs ON rs.relationship_id = r.id JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id LEFT JOIN falukant_type.title t1 ON t1.id = c1.title_of_nobility LEFT JOIN falukant_type.title t2 ON t2.id = c2.title_of_nobility LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id WHERE rs.active = true AND ( rs.last_daily_processed_at IS NULL OR (rs.last_daily_processed_at::date < CURRENT_DATE) ); "#; pub const QUERY_GET_ACTIVE_LOVER_ROWS_FOR_MONTHLY: &str = r#" SELECT r.id AS rel_id, r.character1_id AS c1, r.character2_id AS c2, rs.lover_role, rs.affection, rs.visibility, rs.discretion, rs.maintenance_level, rs.status_fit, rs.monthly_base_cost, rs.scandal_extra_daily_pct, rs.months_underfunded, c1.gender AS g1, c2.gender AS g2, COALESCE(t1.tr, '') AS title1_tr, COALESCE(t2.tr, '') AS title2_tr, COALESCE(c1.reputation, 50)::float8 AS rep1, COALESCE(c2.reputation, 50)::float8 AS rep2, fu1.id AS user1_id, fu2.id AS user2_id, LEAST( ((CURRENT_DATE - c1.birthdate::date) / 365), ((CURRENT_DATE - c2.birthdate::date) / 365) )::int AS min_age_years FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' JOIN falukant_data.relationship_state rs ON rs.relationship_id = r.id JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id LEFT JOIN falukant_type.title t1 ON t1.id = c1.title_of_nobility LEFT JOIN falukant_type.title t2 ON t2.id = c2.title_of_nobility LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id WHERE rs.active = true AND ( rs.last_monthly_processed_at IS NULL OR date_trunc('month', rs.last_monthly_processed_at) < date_trunc('month', CURRENT_TIMESTAMP) ); "#; pub const QUERY_UPDATE_LOVER_VISIBILITY_DISCRETION: &str = r#" UPDATE falukant_data.relationship_state SET visibility = $1::smallint, discretion = $2::smallint WHERE relationship_id = $3::int; "#; pub const QUERY_UPDATE_LOVER_UNDERPAY_STATE: &str = r#" UPDATE falukant_data.relationship_state SET affection = $1::smallint, discretion = $2::smallint, visibility = $3::smallint, months_underfunded = $4::smallint, scandal_extra_daily_pct = $5::smallint WHERE relationship_id = $6::int; "#; pub const QUERY_GET_MARRIAGE_ROWS: &str = r#" SELECT r.id AS marriage_id, r.character1_id AS m1, r.character2_id AS m2, r.marriage_satisfaction, COALESCE(r.marriage_public_stability, 55)::int AS marriage_public_stability, r.marriage_drift_high, r.marriage_drift_low, COALESCE(t1.tr, '') AS title1_tr, COALESCE(t2.tr, '') AS title2_tr, fu1.id AS user1_id, fu2.id AS user2_id, COALESCE(r.marriage_gift_buff_days_remaining, 0)::int AS marriage_gift_buff_days_remaining, COALESCE(r.marriage_pending_feast_bonus, 0)::int AS marriage_pending_feast_bonus, COALESCE(r.marriage_house_supply, 50)::int AS marriage_house_supply, COALESCE(r.marriage_no_lover_bonus_counter, 0)::int AS marriage_no_lover_bonus_counter, COALESCE(( SELECT uh.household_order FROM falukant_data.user_house uh WHERE uh.user_id = c1.user_id ORDER BY uh.id LIMIT 1 ), 55)::int AS household_order_1, COALESCE(( SELECT uh.household_order FROM falukant_data.user_house uh WHERE uh.user_id = c2.user_id ORDER BY uh.id LIMIT 1 ), 55)::int AS household_order_2, COALESCE(( SELECT uh.servant_quality FROM falukant_data.user_house uh WHERE uh.user_id = c1.user_id ORDER BY uh.id LIMIT 1 ), 50)::int AS servant_quality_1, COALESCE(( SELECT uh.servant_quality FROM falukant_data.user_house uh WHERE uh.user_id = c2.user_id ORDER BY uh.id LIMIT 1 ), 50)::int AS servant_quality_2 FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr IN ('married', 'engaged', 'wooing') JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id LEFT JOIN falukant_type.title t1 ON t1.id = c1.title_of_nobility LEFT JOIN falukant_type.title t2 ON t2.id = c2.title_of_nobility LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id; "#; #[allow(dead_code)] // Einfaches Update ohne Buff-Spalten; Daemon nutzt QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS. pub const QUERY_UPDATE_MARRIAGE_STATE: &str = r#" UPDATE falukant_data.relationship SET marriage_satisfaction = $1::smallint, marriage_drift_high = $2::smallint, marriage_drift_low = $3::smallint WHERE id = $4::int; "#; /// Inkl. Geschenk-/Fest-/Haus-Zähler (Migration `003_falukant_family_marriage_buffs.sql`). /// `marriage_public_stability`: Migration `005_falukant_marriage_housepeace.sql`. pub const QUERY_UPDATE_MARRIAGE_STATE_AND_BUFFS: &str = r#" UPDATE falukant_data.relationship SET marriage_satisfaction = $1::smallint, marriage_drift_high = $2::smallint, marriage_drift_low = $3::smallint, marriage_gift_buff_days_remaining = $4::smallint, marriage_pending_feast_bonus = $5::smallint, marriage_no_lover_bonus_counter = $6::smallint, marriage_public_stability = $7::smallint WHERE id = $8::int; "#; /// Persistiert berechneten Hausfrieden (0..100), Migration `005_falukant_marriage_housepeace.sql`. pub const QUERY_UPDATE_USER_HOUSE_TENSION_BY_USER: &str = r#" UPDATE falukant_data.user_house SET household_tension_score = $1::smallint WHERE user_id = $2::int; "#; /// Kinder aus Liebschaften, die einen Charakter dieses Falukant-Users betreffen. pub const QUERY_COUNT_LOVER_CHILDREN_FOR_USER: &str = r#" SELECT COUNT(*)::int AS cnt FROM falukant_data.child_relation cr WHERE cr.birth_context = 'lover' AND ( EXISTS ( SELECT 1 FROM falukant_data.character c WHERE c.id = cr.father_character_id AND c.user_id = $1::int ) OR EXISTS ( SELECT 1 FROM falukant_data.character c WHERE c.id = cr.mother_character_id AND c.user_id = $1::int ) ); "#; /// Haushalt für Spannungsberechnung (ein Eintrag pro Zeile; typisch eine Zeile pro User). pub const QUERY_GET_USER_HOUSE_ROW_BY_USER: &str = r#" SELECT uh.id AS user_house_id, uh.user_id AS falukant_user_id, COALESCE(uh.household_order, 55)::int AS household_order, COALESCE(uh.servant_count, 0)::int AS servant_count, COALESCE(uh.servant_quality, 50)::int AS servant_quality, COALESCE(NULLIF(TRIM(uh.servant_pay_level), ''), 'normal') AS servant_pay_level, COALESCE(uh.household_tension_score, 0)::int AS prev_tension_score FROM falukant_data.user_house uh WHERE uh.user_id = $1::int ORDER BY uh.id LIMIT 1; "#; pub const QUERY_MARRIAGE_SUBTRACT_SATISFACTION: &str = r#" UPDATE falukant_data.relationship r SET marriage_satisfaction = GREATEST(0, r.marriage_satisfaction - $2::int) FROM falukant_type.relationship rt WHERE rt.id = r.relationship_type_id AND rt.tr IN ('married', 'engaged', 'wooing') AND r.id = $1::int; "#; pub const QUERY_RESET_LOVER_UNDERPAY_COUNTERS: &str = r#" UPDATE falukant_data.relationship_state SET months_underfunded = 0 WHERE relationship_id = $1::int; "#; pub const QUERY_MARK_LOVER_DAILY_DONE: &str = r#" UPDATE falukant_data.relationship_state SET last_daily_processed_at = NOW() WHERE relationship_id = $1::int; "#; pub const QUERY_MARK_LOVER_MONTHLY_DONE: &str = r#" UPDATE falukant_data.relationship_state SET last_monthly_processed_at = NOW() WHERE relationship_id = $1::int; "#; /// Liebschaft: fällige Teilzahlung (alle 2 h), Migration `006_falukant_lover_installments.sql`. pub const QUERY_GET_ACTIVE_LOVER_ROWS_FOR_INSTALLMENT: &str = r#" SELECT r.id AS rel_id, r.character1_id AS c1, r.character2_id AS c2, rs.lover_role, rs.affection, rs.visibility, rs.discretion, rs.maintenance_level, rs.status_fit, rs.monthly_base_cost, rs.scandal_extra_daily_pct, rs.months_underfunded, c1.gender AS g1, c2.gender AS g2, COALESCE(t1.tr, '') AS title1_tr, COALESCE(t2.tr, '') AS title2_tr, COALESCE(c1.reputation, 50)::float8 AS rep1, COALESCE(c2.reputation, 50)::float8 AS rep2, fu1.id AS user1_id, fu2.id AS user2_id, LEAST( ((CURRENT_DATE - c1.birthdate::date) / 365), ((CURRENT_DATE - c2.birthdate::date) / 365) )::int AS min_age_years FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' JOIN falukant_data.relationship_state rs ON rs.relationship_id = r.id JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id LEFT JOIN falukant_type.title t1 ON t1.id = c1.title_of_nobility LEFT JOIN falukant_type.title t2 ON t2.id = c2.title_of_nobility LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id WHERE rs.active = true AND ( rs.lover_last_installment_at IS NULL OR rs.lover_last_installment_at < NOW() - INTERVAL '2 hours' ); "#; pub const QUERY_MARK_LOVER_INSTALLMENT_AT: &str = r#" UPDATE falukant_data.relationship_state SET lover_last_installment_at = NOW() WHERE relationship_id = $1::int; "#; pub const QUERY_LOVER_INSTALLMENT_SCHEMA_READY: &str = r#" SELECT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'relationship_state' AND column_name = 'lover_last_installment_at' ) AS ready; "#; pub const QUERY_UPDATE_CHARACTER_REPUTATION: &str = r#" UPDATE falukant_data.character SET reputation = $1::numeric, updated_at = NOW() WHERE id = $2::int; "#; pub const QUERY_GET_LOVER_PREGNANCY_CANDIDATES: &str = r#" SELECT CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END AS father_cid, CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END AS mother_cid, CASE WHEN c1.gender = 'male' THEN c1.title_of_nobility ELSE c2.title_of_nobility END AS title_of_nobility, CASE WHEN c1.gender = 'male' THEN c1.last_name ELSE c2.last_name END AS last_name, CASE WHEN c1.gender = 'male' THEN c1.region_id ELSE c2.region_id END AS region_id, CASE WHEN c1.gender = 'male' THEN fu1.id ELSE fu2.id END AS father_uid, CASE WHEN c1.gender = 'female' THEN fu1.id ELSE fu2.id END AS mother_uid, (CURRENT_DATE - c_female.birthdate::date)::int AS mother_age_days FROM falukant_data.relationship r JOIN falukant_type.relationship rt ON rt.id = r.relationship_type_id AND rt.tr = 'lover' JOIN falukant_data.relationship_state rs ON rs.relationship_id = r.id AND rs.active = true JOIN falukant_data.character c1 ON c1.id = r.character1_id JOIN falukant_data.character c2 ON c2.id = r.character2_id JOIN falukant_data.character c_female ON c_female.id = ( CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END ) LEFT JOIN falukant_data.falukant_user fu1 ON fu1.id = c1.user_id LEFT JOIN falukant_data.falukant_user fu2 ON fu2.id = c2.user_id WHERE (c1.gender = 'female' AND c2.gender = 'male') OR (c1.gender = 'male' AND c2.gender = 'female') AND rs.affection >= 45 AND rs.maintenance_level >= 30 AND rs.last_monthly_processed_at IS NOT NULL AND rs.last_monthly_processed_at >= NOW() - INTERVAL '50 days' AND NOT EXISTS ( SELECT 1 FROM falukant_data.child_relation cr WHERE cr.father_character_id = (CASE WHEN c1.gender = 'male' THEN c1.id ELSE c2.id END) AND cr.mother_character_id = (CASE WHEN c1.gender = 'female' THEN c1.id ELSE c2.id END) AND cr.created_at >= date_trunc('month', CURRENT_TIMESTAMP) ) AND (CURRENT_DATE - c_female.birthdate::date) >= 4380 AND (CURRENT_DATE - c_female.birthdate::date) < 18993 AND random() * 100.0 < ( LEAST(12.0, GREATEST(0.0, CASE rs.lover_role WHEN 'secret_affair' THEN 2.0 WHEN 'lover' THEN 4.0 WHEN 'mistress_or_favorite' THEN 6.0 ELSE 0.0 END + CASE WHEN rs.affection >= 75 THEN 2.0 ELSE 0.0 END + CASE WHEN rs.visibility >= 70 AND rs.affection < 50 THEN -2.0 ELSE 0.0 END + CASE WHEN (CURRENT_DATE - c_female.birthdate::date) > 14600 THEN -3.0 ELSE 0.0 END )) ); "#; pub const QUERY_LOVER_BIRTH_PENALTY_MARRIAGE: &str = r#" UPDATE falukant_data.relationship r SET marriage_satisfaction = GREATEST(0, r.marriage_satisfaction - 8) FROM falukant_type.relationship rt WHERE rt.id = r.relationship_type_id AND rt.tr IN ('married', 'engaged', 'wooing') AND (r.character1_id = $1::int OR r.character2_id = $1::int); "#; pub const QUERY_LOVER_BIRTH_PENALTY_REPUTATION: &str = r#" UPDATE falukant_data.character SET reputation = GREATEST(0::numeric, COALESCE(reputation, 50::numeric) - 4::numeric), updated_at = NOW() WHERE id = $1::int; "#; pub const QUERY_INSERT_CHILD_RELATION_LOVER: &str = r#" INSERT INTO falukant_data.child_relation ( father_character_id, mother_character_id, child_character_id, name_set, legitimacy, birth_context, public_known, created_at, updated_at ) VALUES ( $1::int, $2::int, $3::int, FALSE, 'hidden_bastard', 'lover', FALSE, NOW(), NOW() ); "#; // --- Produktionszertifikat (Daemon Daily, Spec: Produktionszertifikate) --- /// Ein Spielercharakter pro Falukant-User (bei mehreren lebenden: **höchste** `character.id`, /// typischerweise zuletzt aktiver Slot — konsistent mit UI, das oft den Hauptcharakter nutzt). pub const QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS: &str = r#" SELECT DISTINCT ON (fu.id) fu.id AS falukant_user_id, COALESCE(fu.user_id, fu.id)::int AS app_user_id, COALESCE(fu.certificate, 1)::int AS certificate, COALESCE(fu.money, 0)::float8 AS money, c.id AS character_id, COALESCE(c.reputation, 50)::float8 AS reputation, COALESCE(t.level, 0)::int AS title_level, COALESCE(( SELECT AVG(k.knowledge)::float8 FROM falukant_data.knowledge k WHERE k.character_id = c.id ), 0.0) AS avg_knowledge, COALESCE(( SELECT COUNT(*)::bigint FROM falukant_log.production pl WHERE pl.producer_id = fu.id OR pl.producer_id = c.id ), 0) AS completed_production_count, COALESCE(( SELECT MAX(cot.hierarchy_level)::int FROM falukant_data.church_office co JOIN falukant_type.church_office_type cot ON cot.id = co.office_type_id WHERE co.character_id = c.id ), 0) AS max_church_hierarchy, COALESCE(( SELECT STRING_AGG(DISTINCT pot.name, '|') FROM falukant_data.political_office po JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id WHERE po.character_id = c.id AND (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW() ), '') AS political_office_names, COALESCE(( SELECT h.position::int FROM falukant_data.user_house uh JOIN falukant_type.house h ON h.id = uh.house_type_id WHERE uh.user_id = fu.id ORDER BY uh.id LIMIT 1 ), 0) AS house_position FROM falukant_data.falukant_user fu JOIN falukant_data.character c ON c.user_id = fu.id AND c.health > 0 LEFT JOIN falukant_type.title t ON t.id = c.title_of_nobility ORDER BY fu.id, c.id DESC; "#; pub const QUERY_UPDATE_FALUKANT_USER_CERTIFICATE: &str = r#" UPDATE falukant_data.falukant_user SET certificate = $1::int, updated_at = NOW() WHERE id = $2::int; "#; /// Zertifikat + `event_user_id` für WebSocket (`COALESCE(user_id, id)` wie bei `QUERY_GET_PRODUCTION_CERTIFICATE_INPUT_ROWS`). pub const QUERY_GET_FALUKANT_USER_CERT_AND_EVENT: &str = r#" SELECT COALESCE(certificate, 1)::int AS certificate, COALESCE(user_id, id)::int AS event_user_id FROM falukant_data.falukant_user WHERE id = $1::int; "#;