Files
yourpart-daemon/src/worker/sql.rs

3789 lines
148 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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';
"#;
/// Bester Produktstart: Mit **mindestens einem** `falukant_data.vehicle` pro User — bester `worth_percent`
/// je Produkt über alle Filialregionen; **ohne** Fahrzeug nur noch **lokaler** Markt (Region der Direktor-Filiale),
/// sonst wäre der beste Fern-Preis nicht erreichbar.
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,
( (ftp.sell_cost * (
CASE
WHEN EXISTS (
SELECT 1 FROM falukant_data.vehicle v
WHERE v.falukant_user_id = fdu.id
)
THEN bw.max_worth_pct
ELSE COALESCE(fdtpw_local.worth_percent::float8, 0.0)
END / 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 (
SELECT tpw.product_id,
MAX(tpw.worth_percent)::float8 AS max_worth_pct
FROM falukant_data.town_product_worth tpw
WHERE tpw.region_id IN (
SELECT DISTINCT br.region_id
FROM falukant_data.branch br
WHERE br.falukant_user_id = (SELECT d0.employer_user_id FROM falukant_data.director d0 WHERE d0.id = $1::int)
)
GROUP BY tpw.product_id
) bw ON TRUE
JOIN falukant_type.product ftp ON ftp.id = bw.product_id AND ftp.category <= fdu.certificate
LEFT JOIN falukant_data.town_product_worth fdtpw_local
ON fdtpw_local.region_id = fdb.region_id
AND fdtpw_local.product_id = ftp.id
JOIN falukant_data.knowledge fdk_character ON fdk_character.product_id = ftp.id AND fdk_character.character_id = user_character.id
JOIN falukant_data.knowledge fdk_director ON fdk_director.product_id = ftp.id AND fdk_director.character_id = fdd.director_character_id
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();
"#;
/// Spalte `last_political_daily_salary_on` (Migration `013_falukant_political_daily_salary.sql`).
pub const QUERY_POLITICAL_DAILY_SALARY_USER_COLUMN_READY: &str = r#"
SELECT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'falukant_user'
AND column_name = 'last_political_daily_salary_on'
) AS ready;
"#;
/// Aktive Ämter mit Spieler-Charakter, noch **kein** Gehalt heute (UTC).
/// `configured_daily_salary`: Summe aus `political_office_benefit` mit `daily_salary` im JSON; sonst 0 → Fallback im Daemon nach Amts-Rang.
pub const QUERY_POLITICAL_DAILY_SALARY_OFFICE_ROWS: &str = r#"
SELECT
ch.user_id AS falukant_user_id,
COALESCE(pot.name, '') AS office_name,
COALESCE((
SELECT SUM(GREATEST(0, COALESCE(NULLIF(TRIM(sb.value::jsonb->>'daily_salary'), '')::numeric, 0)))
FROM falukant_predefine.political_office_benefit sb
WHERE sb.political_office_type_id = po.office_type_id
AND COALESCE(sb.value::jsonb->>'tr', sb.value::jsonb->>'benefitType') = 'daily_salary'
), 0::numeric) AS configured_daily_salary
FROM falukant_data.political_office po
JOIN falukant_type.political_office_type pot ON pot.id = po.office_type_id
JOIN falukant_data.character ch ON ch.id = po.character_id
JOIN falukant_data.falukant_user fu ON fu.id = ch.user_id
WHERE ch.user_id IS NOT NULL
AND (po.created_at + (pot.term_length * INTERVAL '1 day')) > NOW()
AND (fu.last_political_daily_salary_on IS NULL OR fu.last_political_daily_salary_on < CURRENT_DATE);
"#;
pub const QUERY_UPDATE_LAST_POLITICAL_DAILY_SALARY_ON: &str = r#"
UPDATE falukant_data.falukant_user
SET last_political_daily_salary_on = CURRENT_DATE,
updated_at = NOW()
WHERE id = $1::int
AND (last_political_daily_salary_on IS NULL OR last_political_daily_salary_on < CURRENT_DATE);
"#;
/// 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 (657016000 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;
"#;
/// Aufräumen alter Log-Zeilen (Speicher); Zertifikats-Mindest-Produktionen seit Aufstieg siehe
/// `certificate_productions_count_since` — kein „Reset“ über Löschen nötig.
pub const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#"
DELETE FROM falukant_log.production flp
WHERE DATE(flp.production_timestamp) < CURRENT_DATE - INTERVAL '30 days';
"#;
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 rs.last_monthly_processed_at::date < CURRENT_DATE
);
"#;
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
-- `last_monthly_processed_at` wird im Familien-Tageslauf mitgeführt (1 Spieljahr = 1 Kalendertag).
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 >= CURRENT_DATE
)
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).
/// `completed_production_count`: Produktionen seit `certificate_productions_count_since` (Migration `014`); **NULL** = alle Log-Zeilen (Bestand vor erstem Aufstieg nach Migration).
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)
AND (
fu.certificate_productions_count_since IS NULL
OR COALESCE(
pl.production_timestamp,
pl.production_date::timestamp
) >= fu.certificate_productions_count_since
)
), 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;
"#;
/// Setzt bei jeder Stufenänderung `certificate_productions_count_since` (Mindest-Produktionen / PP neu ab Aufstieg).
pub const QUERY_UPDATE_FALUKANT_USER_CERTIFICATE: &str = r#"
UPDATE falukant_data.falukant_user
SET certificate = $1::int,
certificate_productions_count_since = NOW(),
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;
"#;