Files
kvitt/server/crates/kvitt-server/src/wire/recurring.rs
T
jacobh2 06f48ef9e9 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>
2026-10-17 21:30:20 +02:00

151 lines
5.9 KiB
Rust

//! Recurring template objects (SPEC §14.3.3, §14.3.4 E-28, E-29).
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
use super::expense::SplitMode;
use super::{
DateSchema, OreSchema, PeriodSchema, ServerVersionSchema, TimestampSchema, UuidSchema,
};
/// Exactly one of `share_bp` / `share_ore` non-null.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct TemplateSplit {
#[schema(value_type = UuidSchema)]
pub user_id: Uuid,
#[schema(schema_with = super::nullable_int, required = true)]
pub share_bp: Option<i64>,
#[schema(schema_with = super::nullable_int64, required = true)]
pub share_ore: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct RecurringTemplate {
#[schema(value_type = UuidSchema)]
pub id: Uuid,
#[schema(value_type = UuidSchema)]
pub account_id: Uuid,
#[schema(value_type = UuidSchema)]
pub category_id: Uuid,
#[schema(value_type = OreSchema)]
pub amount_ore: i64,
pub description: String,
pub notes: String,
#[schema(value_type = UuidSchema)]
pub payer_user_id: Uuid,
pub split_mode: SplitMode,
/// Sorted by `user_id`; empty for mode default ([`RecurringTemplate::sort`]).
pub splits: Vec<TemplateSplit>,
#[schema(schema_with = super::int)]
pub day_of_month: i64,
#[schema(value_type = PeriodSchema)]
pub start_period: String,
#[schema(value_type = Option<PeriodSchema>, required = true)]
pub end_period: Option<String>,
pub paused: bool,
#[schema(value_type = Option<PeriodSchema>, required = true)]
pub last_generated_period: Option<String>,
/// Computed with `kvitt_domain::next_occurrence_date` (SPEC §9.5).
#[schema(value_type = Option<DateSchema>, required = true)]
pub next_occurrence_date: Option<String>,
#[schema(value_type = UuidSchema)]
pub created_by_user_id: Uuid,
#[schema(value_type = UuidSchema)]
pub updated_by_user_id: Uuid,
#[schema(value_type = TimestampSchema)]
pub created_at: String,
#[schema(value_type = TimestampSchema)]
pub updated_at: String,
#[schema(value_type = Option<TimestampSchema>, required = true)]
pub deleted_at: Option<String>,
#[schema(value_type = ServerVersionSchema)]
pub server_version: i64,
}
impl RecurringTemplate {
/// Sorts `splits` by `user_id` (SPEC §14.3.3).
pub fn sort(&mut self) {
self.splits.sort_by_key(|s| s.user_id);
}
}
/// E-28 `POST /recurring-templates`. `splits` per SPEC §9.1: none for `default`; two bp entries for `ratio` and
/// `full`; two öre entries summing to the amount for `exact`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct CreateRecurringTemplateRequest {
#[schema(value_type = UuidSchema)]
pub account_id: Uuid,
#[schema(value_type = UuidSchema)]
pub category_id: Uuid,
#[schema(value_type = OreSchema)]
pub amount_ore: i64,
pub description: String,
pub notes: String,
#[schema(value_type = UuidSchema)]
pub payer_user_id: Uuid,
pub split_mode: SplitMode,
pub splits: Vec<TemplateSplit>,
#[schema(schema_with = super::int)]
pub day_of_month: i64,
#[schema(value_type = PeriodSchema)]
pub start_period: String,
#[serde(deserialize_with = "super::required_nullable")]
#[schema(value_type = Option<PeriodSchema>, required = true)]
pub end_period: Option<String>,
pub paused: bool,
}
/// E-29 `PATCH /recurring-templates/{template_id}`: any subset of the E-28 fields; `split_mode` and `splits` must
/// be sent together (the schema's `dependentRequired`, added by [`patch_template_dependent_required`]).
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, ToSchema)]
#[schema(min_properties = 1)]
pub struct PatchRecurringTemplateRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = UuidSchema)]
pub account_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = UuidSchema)]
pub category_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = OreSchema)]
pub amount_ore: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = String)]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = String)]
pub notes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = UuidSchema)]
pub payer_user_id: Option<Uuid>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = SplitMode)]
pub split_mode: Option<SplitMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = Vec<TemplateSplit>)]
pub splits: Option<Vec<TemplateSplit>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(schema_with = super::int)]
pub day_of_month: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = PeriodSchema)]
pub start_period: Option<String>,
#[serde(
default,
deserialize_with = "super::double_option",
skip_serializing_if = "Option::is_none"
)]
#[schema(value_type = Option<PeriodSchema>, required = false)]
pub end_period: Option<Option<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(value_type = bool)]
pub paused: Option<bool>,
}
/// The `dependentRequired` keyword of [`PatchRecurringTemplateRequest`]: `split_mode` and `splits` together. utoipa
/// has no attribute for it, so `openapi.rs` writes it into the generated schema.
pub fn patch_template_dependent_required() -> serde_json::Value {
serde_json::json!({ "split_mode": ["splits"], "splits": ["split_mode"] })
}