M2a: server workspace, kvitt-domain, skeleton routes, OpenAPI match, Dockerfile

Cargo workspace (kvitt-domain, kvitt-server, kvitt-import), Rust 1.99.1 pinned.
kvitt-domain passes every Rust vector and adds next_occurrence_date (D-015).
kvitt-server: config, SQLite pool (WAL, §8.9 pragmas), migration 0001 identical to
the contract schema, error type for all 25 codes, auth extractor and protocol
layer, all 33 routes (E-1/E-2 implemented, the rest 501), utoipa document equal
to contracts/openapi.yaml, seams for M3/M4/M9, serve/migrate/healthcheck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 21:25:35 +02:00
parent c277ee37c2
commit 06f48ef9e9
71 changed files with 9187 additions and 0 deletions
@@ -0,0 +1,79 @@
//! Category objects (SPEC §14.3.3, §14.3.4 E-23, E-24).
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
use super::{ServerVersionSchema, TimestampSchema, UuidSchema};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum DefaultCategoryKey {
Groceries,
Restaurants,
Home,
Utilities,
Transport,
Travel,
Entertainment,
Health,
Shopping,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct Category {
#[schema(value_type = UuidSchema)]
pub id: Uuid,
#[schema(required = true)]
pub default_key: Option<DefaultCategoryKey>,
#[schema(required = true)]
pub name: Option<String>,
/// An icon token of SPEC §9.7.
pub icon: String,
/// A colour token of SPEC §9.7.
pub color: String,
#[schema(schema_with = super::int)]
pub sort_order: i64,
#[schema(value_type = TimestampSchema)]
pub created_at: String,
#[schema(value_type = TimestampSchema)]
pub updated_at: String,
#[schema(value_type = Option<TimestampSchema>, required = true)]
pub archived_at: Option<String>,
#[schema(value_type = Option<TimestampSchema>, required = true)]
pub deleted_at: Option<String>,
#[schema(value_type = ServerVersionSchema)]
pub server_version: i64,
}
/// E-23 `POST /categories`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct CreateCategoryRequest {
pub name: String,
pub icon: String,
pub color: String,
}
/// E-24 `PATCH /categories/{category_id}`. `name`: absent = unchanged (`None`), `null` = reset a default category's
/// name (`Some(None)`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[schema(min_properties = 1)]
pub struct PatchCategoryRequest {
#[serde(
default,
deserialize_with = "super::double_option",
skip_serializing_if = "Option::is_none"
)]
#[schema(value_type = Option<String>, required = false)]
pub name: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = String)]
pub icon: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = String)]
pub color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(schema_with = super::int)]
pub sort_order: Option<i64>,
}