Server overhaul: one place for stored-row reads and the shared inserts
api::rows now also loads settlements, audit entries and the epoch (sync::rows is gone; audit enums parse through rows::token) and owns the expense, split, settlement and allocation inserts that sync push, recurring generation and the importer each had their own copy of (rows::Origin carries the server-owned columns). accounts::insert_account and replace_shares are shared with the importer. Recurring generation moves the per-period expense into create_period_expense. No SQL, wire or log change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -7,12 +7,12 @@ use std::collections::BTreeMap;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use kvitt_domain::{BalanceExpense, BalanceSettlement, compute_balances};
|
||||
use kvitt_server::api::{accounts::name_key, rows};
|
||||
use kvitt_server::api::accounts::{self, name_key};
|
||||
use kvitt_server::api::rows::{self, Origin};
|
||||
use kvitt_server::audit::{AuditEvent, AuditWriter};
|
||||
use kvitt_server::db as server_db;
|
||||
use kvitt_server::error::AppError;
|
||||
use kvitt_server::sync::check::{self, Fail};
|
||||
use kvitt_server::sync::rows as sync_rows;
|
||||
use kvitt_server::time::format_ts;
|
||||
use kvitt_server::wire::{
|
||||
Allocation, AuditAction, AuditSource, EntityType, PushedExpense, PushedSettlement, Split,
|
||||
@@ -66,32 +66,16 @@ async fn find_or_create_account(
|
||||
if let Some(id) = existing {
|
||||
return Ok((id, false));
|
||||
}
|
||||
let id = Uuid::now_v7().to_string();
|
||||
let id = Uuid::now_v7();
|
||||
let version = server_db::next_server_version(conn).await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO accounts (id, name, name_key, created_by_user_id, created_at, updated_at, archived_at, \
|
||||
deleted_at, server_version) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?)",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(name)
|
||||
.bind(&key)
|
||||
.bind(household.owner.to_string())
|
||||
.bind(now_ts)
|
||||
.bind(now_ts)
|
||||
.bind(version)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
for user in household.users {
|
||||
sqlx::query(
|
||||
"INSERT INTO account_default_shares (account_id, user_id, share_bp) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(user.id.to_string())
|
||||
.bind(DEFAULT_SHARE_BP)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
Ok((id, true))
|
||||
accounts::insert_account(conn, id, name, household.owner, now_ts, version).await?;
|
||||
let shares: Vec<(Uuid, i64)> = household
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| (user.id, DEFAULT_SHARE_BP))
|
||||
.collect();
|
||||
accounts::replace_shares(conn, id, &shares).await?;
|
||||
Ok((id.to_string(), true))
|
||||
}
|
||||
|
||||
/// The id of each non-deleted default category, by key.
|
||||
@@ -194,91 +178,20 @@ fn pushed_settlement(
|
||||
})
|
||||
}
|
||||
|
||||
async fn insert_expense(
|
||||
/// The server-owned columns of an imported row: created by the owner, no device, a new `server_version`.
|
||||
async fn origin<'a>(
|
||||
conn: &mut SqliteConnection,
|
||||
p: &PushedExpense,
|
||||
import_key: &str,
|
||||
import_key: &'a str,
|
||||
owner: Uuid,
|
||||
now_ts: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let version = server_db::next_server_version(conn).await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO expenses (id, account_id, category_id, amount_ore, description, notes, expense_date, \
|
||||
payer_user_id, split_mode, recurring_template_id, recurring_period, import_key, created_by_user_id, \
|
||||
updated_by_user_id, updated_by_device_id, created_at, updated_at, deleted_at, server_received_at, \
|
||||
server_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, NULL, ?, ?, NULL, ?, ?)",
|
||||
)
|
||||
.bind(p.id.to_string())
|
||||
.bind(p.account_id.to_string())
|
||||
.bind(p.category_id.to_string())
|
||||
.bind(p.amount_ore)
|
||||
.bind(&p.description)
|
||||
.bind(&p.notes)
|
||||
.bind(&p.expense_date)
|
||||
.bind(p.payer_user_id.to_string())
|
||||
.bind(rows::token_of(&p.split_mode)?)
|
||||
.bind(import_key)
|
||||
.bind(owner.to_string())
|
||||
.bind(owner.to_string())
|
||||
.bind(now_ts)
|
||||
.bind(now_ts)
|
||||
.bind(now_ts)
|
||||
.bind(version)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
for split in &p.splits {
|
||||
sqlx::query(
|
||||
"INSERT INTO expense_splits (expense_id, user_id, share_ore, share_bp) VALUES (?, ?, ?, ?)",
|
||||
)
|
||||
.bind(p.id.to_string())
|
||||
.bind(split.user_id.to_string())
|
||||
.bind(split.share_ore)
|
||||
.bind(split.share_bp)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_settlement(
|
||||
conn: &mut SqliteConnection,
|
||||
p: &PushedSettlement,
|
||||
import_key: &str,
|
||||
owner: Uuid,
|
||||
now_ts: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let version = server_db::next_server_version(conn).await?;
|
||||
sqlx::query(
|
||||
"INSERT INTO settlements (id, from_user_id, to_user_id, amount_ore, settlement_date, note, import_key, \
|
||||
created_by_user_id, updated_by_user_id, updated_by_device_id, created_at, updated_at, deleted_at, \
|
||||
server_received_at, server_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, NULL, ?, ?)",
|
||||
)
|
||||
.bind(p.id.to_string())
|
||||
.bind(p.from_user_id.to_string())
|
||||
.bind(p.to_user_id.to_string())
|
||||
.bind(p.amount_ore)
|
||||
.bind(&p.settlement_date)
|
||||
.bind(&p.note)
|
||||
.bind(import_key)
|
||||
.bind(owner.to_string())
|
||||
.bind(owner.to_string())
|
||||
.bind(now_ts)
|
||||
.bind(now_ts)
|
||||
.bind(now_ts)
|
||||
.bind(version)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
for allocation in &p.allocations {
|
||||
sqlx::query(
|
||||
"INSERT INTO settlement_allocations (settlement_id, account_id, amount_ore) VALUES (?, ?, ?)",
|
||||
)
|
||||
.bind(p.id.to_string())
|
||||
.bind(allocation.account_id.to_string())
|
||||
.bind(allocation.amount_ore)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
now_ts: &'a str,
|
||||
) -> Result<Origin<'a>, AppError> {
|
||||
Ok(Origin {
|
||||
user_id: owner,
|
||||
device_id: None,
|
||||
import_key: Some(import_key),
|
||||
received_at: now_ts,
|
||||
server_version: server_db::next_server_version(conn).await?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn audit_create(
|
||||
@@ -365,7 +278,8 @@ pub async fn write(
|
||||
continue;
|
||||
}
|
||||
};
|
||||
insert_expense(conn, &checked, &row.import_key, owner, &now_ts).await?;
|
||||
let origin = origin(conn, &row.import_key, owner, &now_ts).await?;
|
||||
rows::insert_expense(conn, &checked, None, origin).await?;
|
||||
let stored = rows::expense(conn, checked.id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::internal("imported expense vanished"))?;
|
||||
@@ -391,8 +305,9 @@ pub async fn write(
|
||||
continue;
|
||||
}
|
||||
};
|
||||
insert_settlement(conn, &checked, &row.import_key, owner, &now_ts).await?;
|
||||
let stored = sync_rows::settlement(conn, checked.id)
|
||||
let origin = origin(conn, &row.import_key, owner, &now_ts).await?;
|
||||
rows::insert_settlement(conn, &checked, origin).await?;
|
||||
let stored = rows::settlement(conn, checked.id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::internal("imported settlement vanished"))?;
|
||||
let after = serde_json::to_value(&stored).map_err(AppError::internal)?;
|
||||
|
||||
Reference in New Issue
Block a user