06f48ef9e9
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>
151 lines
5.9 KiB
Rust
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"] })
|
|
}
|