//! 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, #[schema(schema_with = super::nullable_int64, required = true)] pub share_ore: Option, } #[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, #[schema(schema_with = super::int)] pub day_of_month: i64, #[schema(value_type = PeriodSchema)] pub start_period: String, #[schema(value_type = Option, required = true)] pub end_period: Option, pub paused: bool, #[schema(value_type = Option, required = true)] pub last_generated_period: Option, /// Computed with `kvitt_domain::next_occurrence_date` (SPEC §9.5). #[schema(value_type = Option, required = true)] pub next_occurrence_date: Option, #[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, required = true)] pub deleted_at: Option, #[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, #[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, required = true)] pub end_period: Option, 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, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = UuidSchema)] pub category_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = OreSchema)] pub amount_ore: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = String)] pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = String)] pub notes: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = UuidSchema)] pub payer_user_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = SplitMode)] pub split_mode: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = Vec)] pub splits: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(schema_with = super::int)] pub day_of_month: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = PeriodSchema)] pub start_period: Option, #[serde( default, deserialize_with = "super::double_option", skip_serializing_if = "Option::is_none" )] #[schema(value_type = Option, required = false)] pub end_period: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[schema(value_type = bool)] pub paused: Option, } /// 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"] }) }