M12b: category contract v2 on the server (schema 2, fourteen defaults, finer import mapping)

- migrations/0002_categories_v2.sql, byte-identical to contracts/schema/migrations/0002_categories_v2.sql
  (SPEC §14.1.1), with an identity test; SCHEMA_VERSION = 2 (E-1 reports it, the importer's check follows).
- Seed test on a fresh database (the fourteen rows of AC-7.2, server_version 11 to 14) and an upgrade test of a
  v1 database in use (0001 applied through the migrator, then users, a custom, a renamed and an archived default,
  an account, a recurring template, expenses): every earlier row unchanged, the four rows above the old
  sequence, foreign_key_check empty, schema_version 2, a pull from the old cursor returns exactly the four rows.
- DefaultCategoryKey with the fourteen keys (wire enum and contracts/openapi.yaml); icons 33 and colours 14
  (§9.7) in validation.
- The two category examples M12a held back (E-26 response-default, sync/pull-page-categories-v2) and its
  contracts-check patch for them and the enum.
- Importer: splitwise_categories.csv per the §10.8 v0.8 table (D-053), DEFAULT_KEYS of fourteen, synthetic
  fixtures for the finer mapping (Insurance/Försäkringar to insurance among them), a schema-1 refusal test.
- Test harnesses seed users at server_version 15/16 (after the fourteen seeds); pull counts 10 -> 14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 08:23:37 +02:00
parent b09f3220a1
commit 6eaccec7ac
21 changed files with 790 additions and 57 deletions
+28 -5
View File
@@ -9,8 +9,8 @@ use kvitt_domain::Period;
use crate::error::AppError;
use crate::time::STOCKHOLM;
/// Category icon tokens (SPEC §9.7, exhaustive for v1).
pub const ICONS: [&str; 30] = [
/// Category icon tokens (SPEC §9.7, exhaustive; `key`, `shield` and `account_balance` added by D-054).
pub const ICONS: [&str; 33] = [
"shopping_cart",
"restaurant",
"local_cafe",
@@ -41,12 +41,15 @@ pub const ICONS: [&str; 30] = [
"chair",
"subscriptions",
"category",
"key",
"shield",
"account_balance",
];
/// Colour tokens (SPEC §9.7, exhaustive for v1).
pub const COLORS: [&str; 12] = [
/// Colour tokens (SPEC §9.7, exhaustive; `slate` and `amber` added by D-054).
pub const COLORS: [&str; 14] = [
"green", "orange", "brown", "yellow", "blue", "teal", "purple", "red", "pink", "grey",
"indigo", "lime",
"indigo", "lime", "slate", "amber",
];
/// Account name: 1 to 60 code points (SPEC §9.6).
@@ -199,4 +202,24 @@ mod tests {
assert!(color("lime").is_ok());
assert!(color("black").is_err());
}
#[test]
fn tokens_added_by_schema_2() {
for token in ["key", "shield", "account_balance"] {
assert!(icon(token).is_ok(), "{token}");
}
for token in ["slate", "amber"] {
assert!(color(token).is_ok(), "{token}");
}
assert_eq!(ICONS.len(), 33);
assert_eq!(COLORS.len(), 14);
let mut icons = ICONS.to_vec();
icons.sort_unstable();
icons.dedup();
assert_eq!(icons.len(), ICONS.len(), "icon tokens are distinct");
let mut colors = COLORS.to_vec();
colors.sort_unstable();
colors.dedup();
assert_eq!(colors.len(), COLORS.len(), "colour tokens are distinct");
}
}