Integrate debt management features into Falukant daemon: Added falukant_debtors module for handling debtor logic, including daily processing and SQL queries for managing debtors' status and actions. Updated FalukantFamilyWorker to incorporate debtor checks and error handling, enhancing financial interactions and family dynamics.
This commit is contained in:
29
docs/FALUKANT_DEBTORS_DAEMON.md
Normal file
29
docs/FALUKANT_DEBTORS_DAEMON.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Falukant: Schuldturm & Pfändung (Daemon)
|
||||
|
||||
Der externe Daemon (`YpDaemon`) pflegt Verzugstage, Schuldturm-Eintritt, Pfändung und Freilassung. **Keine zusätzliche Migration** im Daemon nötig – Spalten in `falukant_data.debtors_prism` kommen vom Projekt.
|
||||
|
||||
## Wo im Code
|
||||
|
||||
| Komponente | Datei |
|
||||
|------------|--------|
|
||||
| SQL | `src/worker/sql.rs` (`QUERY_DEBTORS_*`) |
|
||||
| Tageslogik | `src/worker/falukant_debtors.rs` |
|
||||
| Stündliche Kreditrate + Verzug-Reset bei Zahlung | `src/worker/user_character.rs` |
|
||||
| Daily-Tick (24 h) | `src/worker/falukant_family.rs` → `falukant_debtors::run_daily` |
|
||||
|
||||
## Ablauf
|
||||
|
||||
1. **Stündlich**: `handle_credits` zieht Rate, wenn genug Geld. Bei Erfolg: `falukant_debtors::on_credit_payment_success` setzt `days_overdue` für `status = delinquent` zurück.
|
||||
2. **Täglich** (gleicher Rhythmus wie Falukant-Family-Daily): Nutzer mit offenem Kredit → Verzug erhöhen / Warnstufen / ab Tag 3 Eintritt `imprisoned` → Geld- und Fahrzeugverwertung → soziale Haftfolgen.
|
||||
|
||||
## Events (WebSocket)
|
||||
|
||||
Primär: `falukantUpdateDebt` mit `reason` (siehe Projektspezifikation). Begleitend: `falukantUpdateStatus`, `falukantUpdateFamily`, `falukantHouseUpdate`, `falukantBranchUpdate` je nach Schritt.
|
||||
|
||||
## Noch offen (größere Versionen)
|
||||
|
||||
- Waren/Lager, Hauspfändung, Niederlassungsschließung
|
||||
- `household_tension_reasons_json` um `debtorsPrison` ergänzen
|
||||
- Beziehungsabbruch nach Spec (Schwellen / Zufall)
|
||||
|
||||
Siehe die vollständige fachliche Spezifikation im Projektdokument „Schuldturm und Pfändung“.
|
||||
440
src/worker/falukant_debtors.rs
Normal file
440
src/worker/falukant_debtors.rs
Normal file
@@ -0,0 +1,440 @@
|
||||
//! Schuldturm, Verzug, Pfändung (täglich). Siehe `docs/FALUKANT_DEBTORS_DAEMON.md`.
|
||||
//! Voraussetzung: erweiterte Spalten in `falukant_data.debtors_prism` (projektseitig).
|
||||
|
||||
use crate::db::DbError;
|
||||
use crate::message_broker::MessageBroker;
|
||||
use crate::worker::base::BaseWorker;
|
||||
use crate::worker::sql::{
|
||||
QUERY_DEBTORS_CREDIT_USERS_FOR_DAILY, QUERY_DEBTORS_DELETE_VEHICLE,
|
||||
QUERY_DEBTORS_ENTER_PRISON, QUERY_DEBTORS_GET_PRISM_BY_CHARACTER,
|
||||
QUERY_DEBTORS_HOUSEHOLD_TENSION_ADD, QUERY_DEBTORS_IMPRISONED_LOVER_AFF_SUB2,
|
||||
QUERY_DEBTORS_IMPRISONED_MARRIAGE_SUB1, QUERY_DEBTORS_IMPRISONED_PENALTY_PLUS1,
|
||||
QUERY_DEBTORS_IMPRISONED_REP_MALUS, QUERY_DEBTORS_IMPRISONED_TENSION_PLUS2,
|
||||
QUERY_DEBTORS_INSERT_DELINQUENT, QUERY_DEBTORS_INCREMENT_DELINQUENT,
|
||||
QUERY_DEBTORS_REACTIVATE_DELINQUENT_FROM_RELEASED,
|
||||
QUERY_DEBTORS_LOVER_AFFECTION_SUB, QUERY_DEBTORS_MARRIAGE_SATISFACTION_SUB,
|
||||
QUERY_DEBTORS_PRISM_SCHEMA_READY, QUERY_DEBTORS_RELEASE_IF_PAID,
|
||||
QUERY_DEBTORS_RESET_DELINQUENCY_SOLVENT, QUERY_DEBTORS_RESET_ON_PAYMENT_SUCCESS,
|
||||
QUERY_DEBTORS_SUBTRACT_REPUTATION, QUERY_DEBTORS_UPDATE_PRISM_REMAINING,
|
||||
QUERY_DEBTORS_UPDATE_PRISM_REMAINING_MONEY, QUERY_DEBTORS_VEHICLE_FOR_SEIZURE,
|
||||
};
|
||||
|
||||
fn can_afford_installments(money: f64, total_pay_rate: f64) -> bool {
|
||||
if total_pay_rate <= 0.0 {
|
||||
return true;
|
||||
}
|
||||
// Wie UserCharacterWorker::process_single_credit: Reserve 3× Rate
|
||||
total_pay_rate <= money - (total_pay_rate * 3.0)
|
||||
}
|
||||
|
||||
fn publish_debt(broker: &MessageBroker, user_id: i32, reason: &str) {
|
||||
let d = format!(
|
||||
r#"{{"event":"falukantUpdateDebt","user_id":{},"reason":"{}"}}"#,
|
||||
user_id, reason
|
||||
);
|
||||
broker.publish(d);
|
||||
let s = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(s);
|
||||
}
|
||||
|
||||
fn publish_family_daily(broker: &MessageBroker, user_id: i32) {
|
||||
let m = format!(
|
||||
r#"{{"event":"falukantUpdateFamily","user_id":{},"reason":"daily"}}"#,
|
||||
user_id
|
||||
);
|
||||
broker.publish(m);
|
||||
}
|
||||
|
||||
fn publish_house(broker: &MessageBroker, user_id: i32) {
|
||||
let m = format!(r#"{{"event":"falukantHouseUpdate","user_id":{}}}"#, user_id);
|
||||
broker.publish(m);
|
||||
}
|
||||
|
||||
fn publish_branch(broker: &MessageBroker, user_id: i32) {
|
||||
let m = format!(r#"{{"event":"falukantBranchUpdate","user_id":{}}}"#, user_id);
|
||||
broker.publish(m);
|
||||
}
|
||||
|
||||
fn parse_i32_row(row: &crate::db::Row, key: &str, default: i32) -> i32 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<i32>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn parse_f64_row(row: &crate::db::Row, key: &str, default: f64) -> f64 {
|
||||
row.get(key)
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Nach erfolgreicher Kreditrate (hourly): Verzug zurücksetzen, solange nicht im Schuldturm.
|
||||
pub fn on_credit_payment_success(base: &BaseWorker, character_id: i32) -> Result<(), DbError> {
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare(
|
||||
"dp_reset_pay",
|
||||
QUERY_DEBTORS_RESET_ON_PAYMENT_SUCCESS,
|
||||
)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_reset_pay: {e}")))?;
|
||||
conn.execute("dp_reset_pay", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_reset_pay: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn schema_ready(pool: &crate::db::ConnectionPool) -> bool {
|
||||
let Ok(mut conn) = pool.get() else {
|
||||
return false;
|
||||
};
|
||||
if conn
|
||||
.prepare("dp_schema", QUERY_DEBTORS_PRISM_SCHEMA_READY)
|
||||
.is_err()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Ok(rows) = conn.execute("dp_schema", &[]) else {
|
||||
return false;
|
||||
};
|
||||
rows.first()
|
||||
.and_then(|r| r.get("ready"))
|
||||
.map(|v| v == "t" || v == "true")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn run_daily(base: &BaseWorker, broker: &MessageBroker) -> Result<(), DbError> {
|
||||
if !schema_ready(&base.pool) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("dp_users", QUERY_DEBTORS_CREDIT_USERS_FOR_DAILY)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_users: {e}")))?;
|
||||
let user_rows = conn
|
||||
.execute("dp_users", &[])
|
||||
.map_err(|e| DbError::new(format!("exec dp_users: {e}")))?;
|
||||
|
||||
for row in user_rows {
|
||||
let user_id = parse_i32_row(&row, "user_id", -1);
|
||||
let character_id = parse_i32_row(&row, "character_id", -1);
|
||||
let money = parse_f64_row(&row, "money", 0.0);
|
||||
let total_pay_rate = parse_f64_row(&row, "total_pay_rate", 0.0);
|
||||
let total_credit_remaining = parse_f64_row(&row, "total_credit_remaining", 0.0);
|
||||
if user_id < 0 || character_id < 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
drop(conn);
|
||||
process_one_user(
|
||||
base,
|
||||
broker,
|
||||
user_id,
|
||||
character_id,
|
||||
money,
|
||||
total_pay_rate,
|
||||
total_credit_remaining,
|
||||
)?;
|
||||
|
||||
conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("dp_users", QUERY_DEBTORS_CREDIT_USERS_FOR_DAILY)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_users: {e}")))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_one_user(
|
||||
base: &BaseWorker,
|
||||
broker: &MessageBroker,
|
||||
user_id: i32,
|
||||
character_id: i32,
|
||||
money: f64,
|
||||
total_pay_rate: f64,
|
||||
total_credit_remaining: f64,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("dp_get", QUERY_DEBTORS_GET_PRISM_BY_CHARACTER)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_get: {e}")))?;
|
||||
let prism_rows = conn
|
||||
.execute("dp_get", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_get: {e}")))?;
|
||||
|
||||
let status = prism_rows
|
||||
.first()
|
||||
.and_then(|r| r.get("status"))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let days_overdue = prism_rows
|
||||
.first()
|
||||
.map(|r| parse_i32_row(r, "days_overdue", 0))
|
||||
.unwrap_or(0);
|
||||
|
||||
let solvent = can_afford_installments(money, total_pay_rate);
|
||||
|
||||
// Freilassung wenn Kredit getilgt
|
||||
conn.prepare("dp_rel", QUERY_DEBTORS_RELEASE_IF_PAID)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_rel: {e}")))?;
|
||||
let released = conn
|
||||
.execute("dp_rel", &[&user_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_rel: {e}")))?;
|
||||
if !released.is_empty() {
|
||||
publish_debt(broker, user_id, "debtors_prison_released");
|
||||
publish_family_daily(broker, user_id);
|
||||
publish_house(broker, user_id);
|
||||
publish_branch(broker, user_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if status == "imprisoned" {
|
||||
apply_imprisoned_daily_effects(base, broker, user_id, character_id)?;
|
||||
seize_assets_step(base, broker, user_id, character_id, money)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Zahlungsfähig: Verzugstage nur bei delinquent zurücksetzen (nicht im Schuldturm).
|
||||
if solvent && status == "delinquent" {
|
||||
conn.prepare("dp_sol", QUERY_DEBTORS_RESET_DELINQUENCY_SOLVENT)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_sol: {e}")))?;
|
||||
conn.execute("dp_sol", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_sol: {e}")))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !solvent && total_credit_remaining > 0.01 {
|
||||
if prism_rows.is_empty() || status.is_empty() {
|
||||
conn.prepare("dp_ins", QUERY_DEBTORS_INSERT_DELINQUENT)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_ins: {e}")))?;
|
||||
let ins = conn
|
||||
.execute("dp_ins", &[&character_id, &total_credit_remaining])
|
||||
.map_err(|e| DbError::new(format!("exec dp_ins: {e}")))?;
|
||||
if !ins.is_empty() {
|
||||
publish_debt(broker, user_id, "delinquency");
|
||||
}
|
||||
} else if status == "released" {
|
||||
conn.prepare("dp_react", QUERY_DEBTORS_REACTIVATE_DELINQUENT_FROM_RELEASED)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_react: {e}")))?;
|
||||
let r = conn
|
||||
.execute("dp_react", &[&character_id, &total_credit_remaining])
|
||||
.map_err(|e| DbError::new(format!("exec dp_react: {e}")))?;
|
||||
if !r.is_empty() {
|
||||
publish_debt(broker, user_id, "delinquency");
|
||||
}
|
||||
} else if status == "delinquent" {
|
||||
conn.prepare("dp_inc", QUERY_DEBTORS_INCREMENT_DELINQUENT)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_inc: {e}")))?;
|
||||
let inc = conn
|
||||
.execute("dp_inc", &[&character_id, &total_credit_remaining])
|
||||
.map_err(|e| DbError::new(format!("exec dp_inc: {e}")))?;
|
||||
if !inc.is_empty() {
|
||||
publish_debt(broker, user_id, "delinquency");
|
||||
}
|
||||
}
|
||||
|
||||
drop(conn);
|
||||
let mut conn2 = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn2
|
||||
.prepare("dp_get2", QUERY_DEBTORS_GET_PRISM_BY_CHARACTER)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_get2: {e}")))?;
|
||||
let pr2 = conn2
|
||||
.execute("dp_get2", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_get2: {e}")))?;
|
||||
let d = pr2
|
||||
.first()
|
||||
.map(|r| parse_i32_row(r, "days_overdue", 0))
|
||||
.unwrap_or(days_overdue);
|
||||
|
||||
if d >= 3 {
|
||||
try_enter_prison(base, broker, user_id, character_id, total_credit_remaining)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_enter_prison(
|
||||
base: &BaseWorker,
|
||||
broker: &MessageBroker,
|
||||
user_id: i32,
|
||||
character_id: i32,
|
||||
total_credit_remaining: f64,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("dp_ent", QUERY_DEBTORS_ENTER_PRISON)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_ent: {e}")))?;
|
||||
let rows = conn
|
||||
.execute("dp_ent", &[&character_id, &total_credit_remaining])
|
||||
.map_err(|e| DbError::new(format!("exec dp_ent: {e}")))?;
|
||||
|
||||
if rows.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
conn.prepare("dp_rep12", QUERY_DEBTORS_SUBTRACT_REPUTATION)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_rep12: {e}")))?;
|
||||
conn.execute("dp_rep12", &[&character_id, &12_i32])
|
||||
.map_err(|e| DbError::new(format!("exec dp_rep12: {e}")))?;
|
||||
|
||||
conn.prepare("dp_mar10", QUERY_DEBTORS_MARRIAGE_SATISFACTION_SUB)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_mar10: {e}")))?;
|
||||
conn.execute("dp_mar10", &[&character_id, &10_i32])
|
||||
.map_err(|e| DbError::new(format!("exec dp_mar10: {e}")))?;
|
||||
|
||||
conn.prepare("dp_tens15", QUERY_DEBTORS_HOUSEHOLD_TENSION_ADD)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_tens15: {e}")))?;
|
||||
conn.execute("dp_tens15", &[&user_id, &15_i32])
|
||||
.map_err(|e| DbError::new(format!("exec dp_tens15: {e}")))?;
|
||||
|
||||
conn.prepare("dp_lov4", QUERY_DEBTORS_LOVER_AFFECTION_SUB)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_lov4: {e}")))?;
|
||||
conn.execute("dp_lov4", &[&character_id, &4_i32])
|
||||
.map_err(|e| DbError::new(format!("exec dp_lov4: {e}")))?;
|
||||
|
||||
publish_debt(broker, user_id, "debtors_prison_entered");
|
||||
publish_family_daily(broker, user_id);
|
||||
publish_house(broker, user_id);
|
||||
publish_branch(broker, user_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_imprisoned_daily_effects(
|
||||
base: &BaseWorker,
|
||||
broker: &MessageBroker,
|
||||
user_id: i32,
|
||||
character_id: i32,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("dp_r2", QUERY_DEBTORS_IMPRISONED_REP_MALUS)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_r2: {e}")))?;
|
||||
conn.execute("dp_r2", &[&character_id, &2_i32])
|
||||
.map_err(|e| DbError::new(format!("exec dp_r2: {e}")))?;
|
||||
|
||||
conn.prepare("dp_p1", QUERY_DEBTORS_IMPRISONED_PENALTY_PLUS1)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_p1: {e}")))?;
|
||||
conn.execute("dp_p1", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_p1: {e}")))?;
|
||||
|
||||
conn.prepare("dp_m1", QUERY_DEBTORS_IMPRISONED_MARRIAGE_SUB1)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_m1: {e}")))?;
|
||||
conn.execute("dp_m1", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_m1: {e}")))?;
|
||||
|
||||
conn.prepare("dp_t2", QUERY_DEBTORS_IMPRISONED_TENSION_PLUS2)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_t2: {e}")))?;
|
||||
conn.execute("dp_t2", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_t2: {e}")))?;
|
||||
|
||||
conn.prepare("dp_la2", QUERY_DEBTORS_IMPRISONED_LOVER_AFF_SUB2)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_la2: {e}")))?;
|
||||
conn.execute("dp_la2", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_la2: {e}")))?;
|
||||
|
||||
publish_family_daily(broker, user_id);
|
||||
let s = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(s);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn seize_assets_step(
|
||||
base: &BaseWorker,
|
||||
broker: &MessageBroker,
|
||||
user_id: i32,
|
||||
character_id: i32,
|
||||
money: f64,
|
||||
) -> Result<(), DbError> {
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
|
||||
conn.prepare("dp_getr", QUERY_DEBTORS_GET_PRISM_BY_CHARACTER)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_getr: {e}")))?;
|
||||
let pr = conn
|
||||
.execute("dp_getr", &[&character_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_getr: {e}")))?;
|
||||
let remaining = pr
|
||||
.first()
|
||||
.map(|r| parse_f64_row(r, "remaining_debt", 0.0))
|
||||
.unwrap_or(0.0);
|
||||
if remaining <= 0.01 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
drop(conn);
|
||||
|
||||
if money > 0.01 {
|
||||
let take = money.min(remaining);
|
||||
base.change_falukant_user_money(user_id, -take, "debtors asset seizure")?;
|
||||
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("dp_rm", QUERY_DEBTORS_UPDATE_PRISM_REMAINING_MONEY)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_rm: {e}")))?;
|
||||
conn.execute("dp_rm", &[&character_id, &take])
|
||||
.map_err(|e| DbError::new(format!("exec dp_rm: {e}")))?;
|
||||
|
||||
publish_debt(broker, user_id, "asset_seizure");
|
||||
publish_branch(broker, user_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut conn = base
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?;
|
||||
conn.prepare("dp_v", QUERY_DEBTORS_VEHICLE_FOR_SEIZURE)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_v: {e}")))?;
|
||||
let vr = conn
|
||||
.execute("dp_v", &[&user_id])
|
||||
.map_err(|e| DbError::new(format!("exec dp_v: {e}")))?;
|
||||
|
||||
if let Some(row) = vr.first() {
|
||||
let vid = parse_i32_row(row, "vehicle_id", -1);
|
||||
let cond = parse_i32_row(row, "veh_condition", 100).max(1) as f64;
|
||||
let type_cost = parse_f64_row(row, "type_cost", 0.0);
|
||||
if vid > 0 && type_cost > 0.0 {
|
||||
let proceeds = (type_cost * (cond / 100.0) * 0.55).max(0.0);
|
||||
conn.prepare("dp_dv", QUERY_DEBTORS_DELETE_VEHICLE)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_dv: {e}")))?;
|
||||
conn.execute("dp_dv", &[&vid])
|
||||
.map_err(|e| DbError::new(format!("exec dp_dv: {e}")))?;
|
||||
|
||||
conn.prepare("dp_rmv", QUERY_DEBTORS_UPDATE_PRISM_REMAINING)
|
||||
.map_err(|e| DbError::new(format!("prepare dp_rmv: {e}")))?;
|
||||
conn.execute("dp_rmv", &[&character_id, &proceeds, &vid])
|
||||
.map_err(|e| DbError::new(format!("exec dp_rmv: {e}")))?;
|
||||
|
||||
publish_debt(broker, user_id, "vehicle_liquidation");
|
||||
let s = format!(r#"{{"event":"falukantUpdateStatus","user_id":{}}}"#, user_id);
|
||||
broker.publish(s);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
//! optional `004` + Backend-Stammdaten für Dienerschaft (`falukant_servants`),
|
||||
//! `005` Ehe öffentliche Stabilität + Hausfrieden (`household_tension_score`),
|
||||
//! `006` Liebschafts-Unterhalt 12× pro Spieltag (alle 2 h), 1 Spieltag = 1 Spieljahr.
|
||||
//! Schuldturm/Verzug/Pfändung: `falukant_debtors::run_daily` (siehe `docs/FALUKANT_DEBTORS_DAEMON.md`).
|
||||
//!
|
||||
//! WebSocket: `falukantUpdateFamily` (reason) + `falukantUpdateStatus` für betroffene Nutzer.
|
||||
|
||||
@@ -154,6 +155,9 @@ impl FalukantFamilyWorker {
|
||||
if self.servants_schema_ready {
|
||||
super::falukant_servants::run_daily(&self.base, &self.base.broker)?;
|
||||
}
|
||||
if let Err(e) = super::falukant_debtors::run_daily(&self.base, &self.base.broker) {
|
||||
eprintln!("[FalukantFamilyWorker] falukant_debtors::run_daily: {e}");
|
||||
}
|
||||
|
||||
let mut conn = self
|
||||
.base
|
||||
|
||||
@@ -14,6 +14,7 @@ mod events;
|
||||
mod falukant_family;
|
||||
mod falukant_certificate;
|
||||
mod falukant_servants;
|
||||
mod falukant_debtors;
|
||||
mod sql;
|
||||
|
||||
pub use base::Worker;
|
||||
|
||||
@@ -1144,9 +1144,278 @@ pub const QUERY_CLEANUP_CREDITS: &str = r#"
|
||||
WHERE remaining_amount <= 0.01;
|
||||
"#;
|
||||
|
||||
pub const QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM: &str = r#"
|
||||
INSERT INTO falukant_data.debtors_prism (character_id)
|
||||
VALUES ($1);
|
||||
// --- 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#"
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::worker::sql::{
|
||||
QUERY_GET_OPEN_CREDITS,
|
||||
QUERY_UPDATE_CREDIT,
|
||||
QUERY_CLEANUP_CREDITS,
|
||||
QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM,
|
||||
QUERY_GET_CURRENT_MONEY,
|
||||
QUERY_GET_HOUSE_VALUE,
|
||||
QUERY_GET_SETTLEMENT_VALUE,
|
||||
@@ -449,10 +448,6 @@ impl UserCharacterWorker {
|
||||
conn.prepare("get_open_credits", QUERY_GET_OPEN_CREDITS)?;
|
||||
conn.prepare("update_credit", QUERY_UPDATE_CREDIT)?;
|
||||
conn.prepare("cleanup_credits", QUERY_CLEANUP_CREDITS)?;
|
||||
conn.prepare(
|
||||
"add_character_to_debtors_prism",
|
||||
QUERY_ADD_CHARACTER_TO_DEBTORS_PRISM,
|
||||
)?;
|
||||
|
||||
let credits_rows = conn.execute("get_open_credits", &[])?;
|
||||
for row in credits_rows {
|
||||
@@ -508,6 +503,12 @@ impl UserCharacterWorker {
|
||||
eprintln!(
|
||||
"[UserCharacterWorker] Fehler bei change_falukant_user_money (credit pay rate): {err}"
|
||||
);
|
||||
} else if let Err(err) =
|
||||
super::falukant_debtors::on_credit_payment_success(&self.base, character_id)
|
||||
{
|
||||
eprintln!(
|
||||
"[UserCharacterWorker] falukant_debtors::on_credit_payment_success: {err}"
|
||||
);
|
||||
}
|
||||
} else if prism_started_previously {
|
||||
if let Err(err) = self
|
||||
@@ -518,9 +519,8 @@ impl UserCharacterWorker {
|
||||
"[UserCharacterWorker] Fehler bei change_falukant_user_money (debitor_prism): {err}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
conn.execute("add_character_to_debtors_prism", &[&character_id])?;
|
||||
}
|
||||
// Verzug / Schuldturm: täglich `falukant_debtors::run_daily` (FalukantFamilyWorker)
|
||||
|
||||
conn.execute("update_credit", &[&remaining_amount, &user_id])?;
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user