Skip to main content

backbone_accounting/presentation/dto/
journal_dto.rs

1//! Journal DTOs (Data Transfer Objects)
2//!
3//! Generated by metaphor-schema. Do not edit manually.
4//!
5//! DTOs provide a clean separation between domain entities and API
6//! representations, with OpenAPI documentation support.
7
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10use chrono::{DateTime, Utc, NaiveDate};
11use rust_decimal::Decimal;
12
13#[cfg(feature = "openapi")]
14#[cfg(feature = "openapi")]
15use utoipa::ToSchema;
16
17use crate::domain::entity::Journal;
18use crate::domain::entity::AuditMetadata;
19use crate::domain::entity::JournalSource;
20use crate::domain::entity::JournalStatus;
21use crate::domain::entity::JournalType;
22
23// =============================================================================
24// Create DTO
25// =============================================================================
26
27/// Request DTO for creating a new Journal
28///
29/// Used in POST requests to create new entities.
30/// Excludes auto-generated fields (id, created_at, updated_at, deleted_at).
31#[derive(Debug, Clone, Deserialize)]
32#[cfg_attr(feature = "openapi", derive(ToSchema))]
33#[serde(rename_all = "camelCase")]
34pub struct CreateJournalDto {
35    #[cfg_attr(feature = "openapi", schema(example = "example"))]
36    #[serde(alias = "journal_number")]
37    pub journal_number: String,
38    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reference_number")]
39    pub reference_number: Option<String>,
40    #[serde(alias = "journal_type")]
41    pub journal_type: JournalType,
42    #[serde(default, skip_serializing_if = "Option::is_none", alias = "branch_id")]
43    pub branch_id: Option<Uuid>,
44    #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
45    #[serde(alias = "transaction_date")]
46    pub transaction_date: NaiveDate,
47    #[serde(default, skip_serializing_if = "Option::is_none", alias = "posting_date")]
48    pub posting_date: Option<NaiveDate>,
49    #[serde(default, skip_serializing_if = "Option::is_none", alias = "fiscal_period_id")]
50    pub fiscal_period_id: Option<Uuid>,
51    #[serde(default, skip_serializing_if = "Option::is_none", alias = "fiscal_year")]
52    pub fiscal_year: Option<i32>,
53    #[serde(default, skip_serializing_if = "Option::is_none", alias = "fiscal_month")]
54    pub fiscal_month: Option<i32>,
55    #[cfg_attr(feature = "openapi", schema(example = "example"))]
56    pub description: String,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub notes: Option<String>,
59    #[cfg_attr(feature = "openapi", schema(example = "example"))]
60    pub currency: String,
61    #[serde(alias = "total_debit")]
62    pub total_debit: Decimal,
63    #[serde(alias = "total_credit")]
64    pub total_credit: Decimal,
65    #[cfg_attr(feature = "openapi", schema(example = 42))]
66    #[serde(alias = "line_count")]
67    pub line_count: i32,
68    pub source: JournalSource,
69    #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_type")]
70    pub source_type: Option<String>,
71    #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_id")]
72    pub source_id: Option<Uuid>,
73    #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_reference")]
74    pub source_reference: Option<String>,
75    #[cfg_attr(feature = "openapi", schema(example = true))]
76    #[serde(alias = "is_reversed")]
77    pub is_reversed: bool,
78    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reversed_by_id")]
79    pub reversed_by_id: Option<Uuid>,
80    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reversed_at")]
81    pub reversed_at: Option<DateTime<Utc>>,
82    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reversal_reason")]
83    pub reversal_reason: Option<String>,
84    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reverses_id")]
85    pub reverses_id: Option<Uuid>,
86    #[cfg_attr(feature = "openapi", schema(example = true))]
87    #[serde(alias = "is_reversing")]
88    pub is_reversing: bool,
89    #[cfg_attr(feature = "openapi", schema(example = true))]
90    #[serde(alias = "auto_reverse")]
91    pub auto_reverse: bool,
92    #[serde(default, skip_serializing_if = "Option::is_none", alias = "auto_reverse_date")]
93    pub auto_reverse_date: Option<NaiveDate>,
94    #[cfg_attr(feature = "openapi", schema(example = true))]
95    #[serde(alias = "requires_approval")]
96    pub requires_approval: bool,
97    #[serde(default, skip_serializing_if = "Option::is_none", alias = "approval_threshold")]
98    pub approval_threshold: Option<Decimal>,
99    #[serde(default, skip_serializing_if = "Option::is_none", alias = "submitted_at")]
100    pub submitted_at: Option<DateTime<Utc>>,
101    #[serde(default, skip_serializing_if = "Option::is_none", alias = "submitted_by")]
102    pub submitted_by: Option<Uuid>,
103    #[serde(default, skip_serializing_if = "Option::is_none", alias = "approved_at")]
104    pub approved_at: Option<DateTime<Utc>>,
105    #[serde(default, skip_serializing_if = "Option::is_none", alias = "approved_by")]
106    pub approved_by: Option<Uuid>,
107    #[serde(default, skip_serializing_if = "Option::is_none", alias = "rejected_at")]
108    pub rejected_at: Option<DateTime<Utc>>,
109    #[serde(default, skip_serializing_if = "Option::is_none", alias = "rejected_by")]
110    pub rejected_by: Option<Uuid>,
111    #[serde(default, skip_serializing_if = "Option::is_none", alias = "rejection_reason")]
112    pub rejection_reason: Option<String>,
113    #[serde(default, skip_serializing_if = "Option::is_none", alias = "posted_at")]
114    pub posted_at: Option<DateTime<Utc>>,
115    #[serde(default, skip_serializing_if = "Option::is_none", alias = "posted_by")]
116    pub posted_by: Option<Uuid>,
117    #[cfg_attr(feature = "openapi", schema(example = true))]
118    #[serde(alias = "is_voided")]
119    pub is_voided: bool,
120    #[serde(default, skip_serializing_if = "Option::is_none", alias = "voided_at")]
121    pub voided_at: Option<DateTime<Utc>>,
122    #[serde(default, skip_serializing_if = "Option::is_none", alias = "voided_by")]
123    pub voided_by: Option<Uuid>,
124    #[serde(default, skip_serializing_if = "Option::is_none", alias = "void_reason")]
125    pub void_reason: Option<String>,
126    pub attachments: serde_json::Value,
127}
128
129// =============================================================================
130// Update DTO
131// =============================================================================
132
133/// Request DTO for full update of a Journal
134///
135/// Used in PUT requests for full entity replacement.
136/// All fields are required (except auto-generated ones).
137#[derive(Debug, Clone, Deserialize)]
138#[cfg_attr(feature = "openapi", derive(ToSchema))]
139#[serde(rename_all = "camelCase")]
140pub struct UpdateJournalDto {
141    #[cfg_attr(feature = "openapi", schema(example = "example"))]
142    #[serde(alias = "journal_number")]
143    pub journal_number: String,
144    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reference_number")]
145    pub reference_number: Option<String>,
146    #[serde(alias = "journal_type")]
147    pub journal_type: JournalType,
148    #[serde(default, skip_serializing_if = "Option::is_none", alias = "branch_id")]
149    pub branch_id: Option<Uuid>,
150    #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
151    #[serde(alias = "transaction_date")]
152    pub transaction_date: NaiveDate,
153    #[serde(default, skip_serializing_if = "Option::is_none", alias = "posting_date")]
154    pub posting_date: Option<NaiveDate>,
155    #[serde(default, skip_serializing_if = "Option::is_none", alias = "fiscal_period_id")]
156    pub fiscal_period_id: Option<Uuid>,
157    #[serde(default, skip_serializing_if = "Option::is_none", alias = "fiscal_year")]
158    pub fiscal_year: Option<i32>,
159    #[serde(default, skip_serializing_if = "Option::is_none", alias = "fiscal_month")]
160    pub fiscal_month: Option<i32>,
161    #[cfg_attr(feature = "openapi", schema(example = "example"))]
162    pub description: String,
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub notes: Option<String>,
165    #[cfg_attr(feature = "openapi", schema(example = "example"))]
166    pub currency: String,
167    #[serde(alias = "total_debit")]
168    pub total_debit: Decimal,
169    #[serde(alias = "total_credit")]
170    pub total_credit: Decimal,
171    #[cfg_attr(feature = "openapi", schema(example = 42))]
172    #[serde(alias = "line_count")]
173    pub line_count: i32,
174    pub source: JournalSource,
175    #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_type")]
176    pub source_type: Option<String>,
177    #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_id")]
178    pub source_id: Option<Uuid>,
179    #[serde(default, skip_serializing_if = "Option::is_none", alias = "source_reference")]
180    pub source_reference: Option<String>,
181    #[cfg_attr(feature = "openapi", schema(example = true))]
182    #[serde(alias = "is_reversed")]
183    pub is_reversed: bool,
184    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reversed_by_id")]
185    pub reversed_by_id: Option<Uuid>,
186    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reversed_at")]
187    pub reversed_at: Option<DateTime<Utc>>,
188    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reversal_reason")]
189    pub reversal_reason: Option<String>,
190    #[serde(default, skip_serializing_if = "Option::is_none", alias = "reverses_id")]
191    pub reverses_id: Option<Uuid>,
192    #[cfg_attr(feature = "openapi", schema(example = true))]
193    #[serde(alias = "is_reversing")]
194    pub is_reversing: bool,
195    #[cfg_attr(feature = "openapi", schema(example = true))]
196    #[serde(alias = "auto_reverse")]
197    pub auto_reverse: bool,
198    #[serde(default, skip_serializing_if = "Option::is_none", alias = "auto_reverse_date")]
199    pub auto_reverse_date: Option<NaiveDate>,
200    pub status: JournalStatus,
201    #[cfg_attr(feature = "openapi", schema(example = true))]
202    #[serde(alias = "requires_approval")]
203    pub requires_approval: bool,
204    #[serde(default, skip_serializing_if = "Option::is_none", alias = "approval_threshold")]
205    pub approval_threshold: Option<Decimal>,
206    #[serde(default, skip_serializing_if = "Option::is_none", alias = "submitted_at")]
207    pub submitted_at: Option<DateTime<Utc>>,
208    #[serde(default, skip_serializing_if = "Option::is_none", alias = "submitted_by")]
209    pub submitted_by: Option<Uuid>,
210    #[serde(default, skip_serializing_if = "Option::is_none", alias = "approved_at")]
211    pub approved_at: Option<DateTime<Utc>>,
212    #[serde(default, skip_serializing_if = "Option::is_none", alias = "approved_by")]
213    pub approved_by: Option<Uuid>,
214    #[serde(default, skip_serializing_if = "Option::is_none", alias = "rejected_at")]
215    pub rejected_at: Option<DateTime<Utc>>,
216    #[serde(default, skip_serializing_if = "Option::is_none", alias = "rejected_by")]
217    pub rejected_by: Option<Uuid>,
218    #[serde(default, skip_serializing_if = "Option::is_none", alias = "rejection_reason")]
219    pub rejection_reason: Option<String>,
220    #[serde(default, skip_serializing_if = "Option::is_none", alias = "posted_at")]
221    pub posted_at: Option<DateTime<Utc>>,
222    #[serde(default, skip_serializing_if = "Option::is_none", alias = "posted_by")]
223    pub posted_by: Option<Uuid>,
224    #[cfg_attr(feature = "openapi", schema(example = true))]
225    #[serde(alias = "is_voided")]
226    pub is_voided: bool,
227    #[serde(default, skip_serializing_if = "Option::is_none", alias = "voided_at")]
228    pub voided_at: Option<DateTime<Utc>>,
229    #[serde(default, skip_serializing_if = "Option::is_none", alias = "voided_by")]
230    pub voided_by: Option<Uuid>,
231    #[serde(default, skip_serializing_if = "Option::is_none", alias = "void_reason")]
232    pub void_reason: Option<String>,
233    pub attachments: serde_json::Value,
234}
235
236// =============================================================================
237// Patch DTO
238// =============================================================================
239
240/// Request DTO for partial update of a Journal
241///
242/// Used in PATCH requests for partial updates.
243/// All fields are optional - only provided fields will be updated.
244#[derive(Debug, Clone, Default, Deserialize)]
245#[cfg_attr(feature = "openapi", derive(ToSchema))]
246#[serde(rename_all = "camelCase")]
247pub struct PatchJournalDto {
248    #[cfg_attr(feature = "openapi", schema(example = "example"))]
249    #[serde(skip_serializing_if = "Option::is_none", alias = "journal_number")]
250    pub journal_number: Option<String>,
251    #[serde(skip_serializing_if = "Option::is_none", alias = "reference_number")]
252    pub reference_number: Option<String>,
253    #[serde(skip_serializing_if = "Option::is_none", alias = "journal_type")]
254    pub journal_type: Option<JournalType>,
255    #[serde(skip_serializing_if = "Option::is_none", alias = "branch_id")]
256    pub branch_id: Option<Uuid>,
257    #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
258    #[serde(skip_serializing_if = "Option::is_none", alias = "transaction_date")]
259    pub transaction_date: Option<NaiveDate>,
260    #[serde(skip_serializing_if = "Option::is_none", alias = "posting_date")]
261    pub posting_date: Option<NaiveDate>,
262    #[serde(skip_serializing_if = "Option::is_none", alias = "fiscal_period_id")]
263    pub fiscal_period_id: Option<Uuid>,
264    #[serde(skip_serializing_if = "Option::is_none", alias = "fiscal_year")]
265    pub fiscal_year: Option<i32>,
266    #[serde(skip_serializing_if = "Option::is_none", alias = "fiscal_month")]
267    pub fiscal_month: Option<i32>,
268    #[cfg_attr(feature = "openapi", schema(example = "example"))]
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub description: Option<String>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub notes: Option<String>,
273    #[cfg_attr(feature = "openapi", schema(example = "example"))]
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub currency: Option<String>,
276    #[serde(skip_serializing_if = "Option::is_none", alias = "total_debit")]
277    pub total_debit: Option<Decimal>,
278    #[serde(skip_serializing_if = "Option::is_none", alias = "total_credit")]
279    pub total_credit: Option<Decimal>,
280    #[cfg_attr(feature = "openapi", schema(example = 42))]
281    #[serde(skip_serializing_if = "Option::is_none", alias = "line_count")]
282    pub line_count: Option<i32>,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub source: Option<JournalSource>,
285    #[serde(skip_serializing_if = "Option::is_none", alias = "source_type")]
286    pub source_type: Option<String>,
287    #[serde(skip_serializing_if = "Option::is_none", alias = "source_id")]
288    pub source_id: Option<Uuid>,
289    #[serde(skip_serializing_if = "Option::is_none", alias = "source_reference")]
290    pub source_reference: Option<String>,
291    #[cfg_attr(feature = "openapi", schema(example = true))]
292    #[serde(skip_serializing_if = "Option::is_none", alias = "is_reversed")]
293    pub is_reversed: Option<bool>,
294    #[serde(skip_serializing_if = "Option::is_none", alias = "reversed_by_id")]
295    pub reversed_by_id: Option<Uuid>,
296    #[serde(skip_serializing_if = "Option::is_none", alias = "reversed_at")]
297    pub reversed_at: Option<DateTime<Utc>>,
298    #[serde(skip_serializing_if = "Option::is_none", alias = "reversal_reason")]
299    pub reversal_reason: Option<String>,
300    #[serde(skip_serializing_if = "Option::is_none", alias = "reverses_id")]
301    pub reverses_id: Option<Uuid>,
302    #[cfg_attr(feature = "openapi", schema(example = true))]
303    #[serde(skip_serializing_if = "Option::is_none", alias = "is_reversing")]
304    pub is_reversing: Option<bool>,
305    #[cfg_attr(feature = "openapi", schema(example = true))]
306    #[serde(skip_serializing_if = "Option::is_none", alias = "auto_reverse")]
307    pub auto_reverse: Option<bool>,
308    #[serde(skip_serializing_if = "Option::is_none", alias = "auto_reverse_date")]
309    pub auto_reverse_date: Option<NaiveDate>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    pub status: Option<JournalStatus>,
312    #[cfg_attr(feature = "openapi", schema(example = true))]
313    #[serde(skip_serializing_if = "Option::is_none", alias = "requires_approval")]
314    pub requires_approval: Option<bool>,
315    #[serde(skip_serializing_if = "Option::is_none", alias = "approval_threshold")]
316    pub approval_threshold: Option<Decimal>,
317    #[serde(skip_serializing_if = "Option::is_none", alias = "submitted_at")]
318    pub submitted_at: Option<DateTime<Utc>>,
319    #[serde(skip_serializing_if = "Option::is_none", alias = "submitted_by")]
320    pub submitted_by: Option<Uuid>,
321    #[serde(skip_serializing_if = "Option::is_none", alias = "approved_at")]
322    pub approved_at: Option<DateTime<Utc>>,
323    #[serde(skip_serializing_if = "Option::is_none", alias = "approved_by")]
324    pub approved_by: Option<Uuid>,
325    #[serde(skip_serializing_if = "Option::is_none", alias = "rejected_at")]
326    pub rejected_at: Option<DateTime<Utc>>,
327    #[serde(skip_serializing_if = "Option::is_none", alias = "rejected_by")]
328    pub rejected_by: Option<Uuid>,
329    #[serde(skip_serializing_if = "Option::is_none", alias = "rejection_reason")]
330    pub rejection_reason: Option<String>,
331    #[serde(skip_serializing_if = "Option::is_none", alias = "posted_at")]
332    pub posted_at: Option<DateTime<Utc>>,
333    #[serde(skip_serializing_if = "Option::is_none", alias = "posted_by")]
334    pub posted_by: Option<Uuid>,
335    #[cfg_attr(feature = "openapi", schema(example = true))]
336    #[serde(skip_serializing_if = "Option::is_none", alias = "is_voided")]
337    pub is_voided: Option<bool>,
338    #[serde(skip_serializing_if = "Option::is_none", alias = "voided_at")]
339    pub voided_at: Option<DateTime<Utc>>,
340    #[serde(skip_serializing_if = "Option::is_none", alias = "voided_by")]
341    pub voided_by: Option<Uuid>,
342    #[serde(skip_serializing_if = "Option::is_none", alias = "void_reason")]
343    pub void_reason: Option<String>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub attachments: Option<serde_json::Value>,
346}
347
348impl PatchJournalDto {
349    /// Check if any field is set
350    pub fn has_changes(&self) -> bool {
351        self.journal_number.is_some() || self.reference_number.is_some() || self.journal_type.is_some() || self.branch_id.is_some() || self.transaction_date.is_some() || self.posting_date.is_some() || self.fiscal_period_id.is_some() || self.fiscal_year.is_some() || self.fiscal_month.is_some() || self.description.is_some() || self.notes.is_some() || self.currency.is_some() || self.total_debit.is_some() || self.total_credit.is_some() || self.line_count.is_some() || self.source.is_some() || self.source_type.is_some() || self.source_id.is_some() || self.source_reference.is_some() || self.is_reversed.is_some() || self.reversed_by_id.is_some() || self.reversed_at.is_some() || self.reversal_reason.is_some() || self.reverses_id.is_some() || self.is_reversing.is_some() || self.auto_reverse.is_some() || self.auto_reverse_date.is_some() || self.status.is_some() || self.requires_approval.is_some() || self.approval_threshold.is_some() || self.submitted_at.is_some() || self.submitted_by.is_some() || self.approved_at.is_some() || self.approved_by.is_some() || self.rejected_at.is_some() || self.rejected_by.is_some() || self.rejection_reason.is_some() || self.posted_at.is_some() || self.posted_by.is_some() || self.is_voided.is_some() || self.voided_at.is_some() || self.voided_by.is_some() || self.void_reason.is_some() || self.attachments.is_some()
352    }
353}
354
355// =============================================================================
356// Response DTO
357// =============================================================================
358
359/// Response DTO for Journal entity
360///
361/// Used in API responses for single entity.
362/// Includes all fields including metadata.
363#[derive(Debug, Clone, Serialize)]
364#[cfg_attr(feature = "openapi", derive(ToSchema))]
365#[serde(rename_all = "camelCase")]
366pub struct JournalResponseDto {
367    #[cfg_attr(feature = "openapi", schema(example = "550e8400-e29b-41d4-a716-446655440000"))]
368    pub id: Uuid,
369    #[cfg_attr(feature = "openapi", schema(example = "example"))]
370    pub journal_number: String,
371    pub reference_number: Option<String>,
372    pub journal_type: JournalType,
373    pub branch_id: Option<Uuid>,
374    #[cfg_attr(feature = "openapi", schema(example = "2024-01-01"))]
375    pub transaction_date: NaiveDate,
376    pub posting_date: Option<NaiveDate>,
377    pub fiscal_period_id: Option<Uuid>,
378    pub fiscal_year: Option<i32>,
379    pub fiscal_month: Option<i32>,
380    #[cfg_attr(feature = "openapi", schema(example = "example"))]
381    pub description: String,
382    pub notes: Option<String>,
383    #[cfg_attr(feature = "openapi", schema(example = "example"))]
384    pub currency: String,
385    pub total_debit: Decimal,
386    pub total_credit: Decimal,
387    #[cfg_attr(feature = "openapi", schema(example = 42))]
388    pub line_count: i32,
389    pub source: JournalSource,
390    pub source_type: Option<String>,
391    pub source_id: Option<Uuid>,
392    pub source_reference: Option<String>,
393    #[cfg_attr(feature = "openapi", schema(example = true))]
394    pub is_reversed: bool,
395    pub reversed_by_id: Option<Uuid>,
396    pub reversed_at: Option<DateTime<Utc>>,
397    pub reversal_reason: Option<String>,
398    pub reverses_id: Option<Uuid>,
399    #[cfg_attr(feature = "openapi", schema(example = true))]
400    pub is_reversing: bool,
401    #[cfg_attr(feature = "openapi", schema(example = true))]
402    pub auto_reverse: bool,
403    pub auto_reverse_date: Option<NaiveDate>,
404    pub status: JournalStatus,
405    #[cfg_attr(feature = "openapi", schema(example = true))]
406    pub requires_approval: bool,
407    pub approval_threshold: Option<Decimal>,
408    pub submitted_at: Option<DateTime<Utc>>,
409    pub submitted_by: Option<Uuid>,
410    pub approved_at: Option<DateTime<Utc>>,
411    pub approved_by: Option<Uuid>,
412    pub rejected_at: Option<DateTime<Utc>>,
413    pub rejected_by: Option<Uuid>,
414    pub rejection_reason: Option<String>,
415    pub posted_at: Option<DateTime<Utc>>,
416    pub posted_by: Option<Uuid>,
417    #[cfg_attr(feature = "openapi", schema(example = true))]
418    pub is_voided: bool,
419    pub voided_at: Option<DateTime<Utc>>,
420    pub voided_by: Option<Uuid>,
421    pub void_reason: Option<String>,
422    pub attachments: serde_json::Value,
423    pub metadata: AuditMetadata,
424}
425
426// =============================================================================
427// List Response DTO
428// =============================================================================
429
430/// Paginated list response for Journal entities
431///
432/// Used in API responses for list endpoints.
433/// Includes pagination metadata.
434#[derive(Debug, Clone, Serialize)]
435#[cfg_attr(feature = "openapi", derive(ToSchema))]
436#[serde(rename_all = "camelCase")]
437pub struct JournalListResponseDto {
438    /// List of items
439    pub items: Vec<JournalResponseDto>,
440    /// Total number of items (across all pages)
441    pub total: u64,
442    /// Current page number (1-indexed)
443    pub page: u32,
444    /// Number of items per page
445    pub per_page: u32,
446    /// Total number of pages
447    pub total_pages: u32,
448    /// Whether there is a next page
449    pub has_next: bool,
450    /// Whether there is a previous page
451    pub has_prev: bool,
452}
453
454impl JournalListResponseDto {
455    /// Create a new list response from items and pagination info
456    pub fn new(items: Vec<JournalResponseDto>, total: u64, page: u32, per_page: u32) -> Self {
457        let total_pages = if per_page > 0 {
458            ((total as f64) / (per_page as f64)).ceil() as u32
459        } else {
460            0
461        };
462        Self {
463            items,
464            total,
465            page,
466            per_page,
467            total_pages,
468            has_next: page < total_pages,
469            has_prev: page > 1,
470        }
471    }
472}
473
474/// Summary DTO for list views (compact version)
475#[derive(Debug, Clone, Serialize)]
476#[cfg_attr(feature = "openapi", derive(ToSchema))]
477#[serde(rename_all = "camelCase")]
478pub struct JournalSummaryDto {
479    pub id: Uuid,
480    pub journal_number: String,
481    pub reference_number: Option<String>,
482    pub journal_type: JournalType,
483    pub created_at: Option<DateTime<Utc>>,
484}
485
486// =============================================================================
487// Conversions
488// =============================================================================
489
490impl From<Journal> for JournalResponseDto {
491    fn from(entity: Journal) -> Self {
492        Self {
493            id: entity.id,
494            journal_number: entity.journal_number,
495            reference_number: entity.reference_number,
496            journal_type: entity.journal_type,
497            branch_id: entity.branch_id,
498            transaction_date: entity.transaction_date,
499            posting_date: entity.posting_date,
500            fiscal_period_id: entity.fiscal_period_id,
501            fiscal_year: entity.fiscal_year,
502            fiscal_month: entity.fiscal_month,
503            description: entity.description,
504            notes: entity.notes,
505            currency: entity.currency,
506            total_debit: entity.total_debit,
507            total_credit: entity.total_credit,
508            line_count: entity.line_count,
509            source: entity.source,
510            source_type: entity.source_type,
511            source_id: entity.source_id,
512            source_reference: entity.source_reference,
513            is_reversed: entity.is_reversed,
514            reversed_by_id: entity.reversed_by_id,
515            reversed_at: entity.reversed_at,
516            reversal_reason: entity.reversal_reason,
517            reverses_id: entity.reverses_id,
518            is_reversing: entity.is_reversing,
519            auto_reverse: entity.auto_reverse,
520            auto_reverse_date: entity.auto_reverse_date,
521            status: entity.status,
522            requires_approval: entity.requires_approval,
523            approval_threshold: entity.approval_threshold,
524            submitted_at: entity.submitted_at,
525            submitted_by: entity.submitted_by,
526            approved_at: entity.approved_at,
527            approved_by: entity.approved_by,
528            rejected_at: entity.rejected_at,
529            rejected_by: entity.rejected_by,
530            rejection_reason: entity.rejection_reason,
531            posted_at: entity.posted_at,
532            posted_by: entity.posted_by,
533            is_voided: entity.is_voided,
534            voided_at: entity.voided_at,
535            voided_by: entity.voided_by,
536            void_reason: entity.void_reason,
537            attachments: entity.attachments,
538            metadata: entity.metadata,
539        }
540    }
541}
542
543impl From<Journal> for JournalSummaryDto {
544    fn from(entity: Journal) -> Self {
545        let created_at = backbone_core::PersistentEntity::created_at(&entity);
546        Self {
547            id: entity.id,
548            journal_number: entity.journal_number,
549            reference_number: entity.reference_number,
550            journal_type: entity.journal_type,
551            created_at,
552        }
553    }
554}
555
556impl From<CreateJournalDto> for Journal {
557    fn from(dto: CreateJournalDto) -> Self {
558        Self {
559            id: Uuid::new_v4(),
560            journal_number: dto.journal_number,
561            reference_number: dto.reference_number,
562            journal_type: dto.journal_type,
563            branch_id: dto.branch_id,
564            transaction_date: dto.transaction_date,
565            posting_date: dto.posting_date,
566            fiscal_period_id: dto.fiscal_period_id,
567            fiscal_year: dto.fiscal_year,
568            fiscal_month: dto.fiscal_month,
569            description: dto.description,
570            notes: dto.notes,
571            currency: dto.currency,
572            total_debit: dto.total_debit,
573            total_credit: dto.total_credit,
574            line_count: dto.line_count,
575            source: dto.source,
576            source_type: dto.source_type,
577            source_id: dto.source_id,
578            source_reference: dto.source_reference,
579            is_reversed: dto.is_reversed,
580            reversed_by_id: dto.reversed_by_id,
581            reversed_at: dto.reversed_at,
582            reversal_reason: dto.reversal_reason,
583            reverses_id: dto.reverses_id,
584            is_reversing: dto.is_reversing,
585            auto_reverse: dto.auto_reverse,
586            auto_reverse_date: dto.auto_reverse_date,
587            status: JournalStatus::Draft,
588            requires_approval: dto.requires_approval,
589            approval_threshold: dto.approval_threshold,
590            submitted_at: dto.submitted_at,
591            submitted_by: dto.submitted_by,
592            approved_at: dto.approved_at,
593            approved_by: dto.approved_by,
594            rejected_at: dto.rejected_at,
595            rejected_by: dto.rejected_by,
596            rejection_reason: dto.rejection_reason,
597            posted_at: dto.posted_at,
598            posted_by: dto.posted_by,
599            is_voided: dto.is_voided,
600            voided_at: dto.voided_at,
601            voided_by: dto.voided_by,
602            void_reason: dto.void_reason,
603            attachments: dto.attachments,
604            metadata: AuditMetadata::default(),
605        }
606    }
607}
608
609impl From<&Journal> for JournalResponseDto {
610    fn from(entity: &Journal) -> Self {
611        Self {
612            id: entity.id.clone(),
613            journal_number: entity.journal_number.clone(),
614            reference_number: entity.reference_number.clone(),
615            journal_type: entity.journal_type.clone(),
616            branch_id: entity.branch_id.clone(),
617            transaction_date: entity.transaction_date.clone(),
618            posting_date: entity.posting_date.clone(),
619            fiscal_period_id: entity.fiscal_period_id.clone(),
620            fiscal_year: entity.fiscal_year.clone(),
621            fiscal_month: entity.fiscal_month.clone(),
622            description: entity.description.clone(),
623            notes: entity.notes.clone(),
624            currency: entity.currency.clone(),
625            total_debit: entity.total_debit.clone(),
626            total_credit: entity.total_credit.clone(),
627            line_count: entity.line_count.clone(),
628            source: entity.source.clone(),
629            source_type: entity.source_type.clone(),
630            source_id: entity.source_id.clone(),
631            source_reference: entity.source_reference.clone(),
632            is_reversed: entity.is_reversed.clone(),
633            reversed_by_id: entity.reversed_by_id.clone(),
634            reversed_at: entity.reversed_at.clone(),
635            reversal_reason: entity.reversal_reason.clone(),
636            reverses_id: entity.reverses_id.clone(),
637            is_reversing: entity.is_reversing.clone(),
638            auto_reverse: entity.auto_reverse.clone(),
639            auto_reverse_date: entity.auto_reverse_date.clone(),
640            status: entity.status.clone(),
641            requires_approval: entity.requires_approval.clone(),
642            approval_threshold: entity.approval_threshold.clone(),
643            submitted_at: entity.submitted_at.clone(),
644            submitted_by: entity.submitted_by.clone(),
645            approved_at: entity.approved_at.clone(),
646            approved_by: entity.approved_by.clone(),
647            rejected_at: entity.rejected_at.clone(),
648            rejected_by: entity.rejected_by.clone(),
649            rejection_reason: entity.rejection_reason.clone(),
650            posted_at: entity.posted_at.clone(),
651            posted_by: entity.posted_by.clone(),
652            is_voided: entity.is_voided.clone(),
653            voided_at: entity.voided_at.clone(),
654            voided_by: entity.voided_by.clone(),
655            void_reason: entity.void_reason.clone(),
656            attachments: entity.attachments.clone(),
657            metadata: entity.metadata.clone(),
658        }
659    }
660}
661
662impl backbone_core::FromCreateDto<CreateJournalDto> for Journal {
663    fn from_create_dto(dto: CreateJournalDto) -> backbone_core::ServiceResult<Self> {
664        Ok(Journal::from(dto))
665    }
666}
667
668impl backbone_core::ApplyUpdateDto<UpdateJournalDto> for Journal {
669    fn apply_update(mut self, dto: UpdateJournalDto) -> backbone_core::ServiceResult<Self> {
670        self.journal_number = dto.journal_number;
671        self.reference_number = dto.reference_number;
672        self.journal_type = dto.journal_type;
673        self.branch_id = dto.branch_id;
674        self.transaction_date = dto.transaction_date;
675        self.posting_date = dto.posting_date;
676        self.fiscal_period_id = dto.fiscal_period_id;
677        self.fiscal_year = dto.fiscal_year;
678        self.fiscal_month = dto.fiscal_month;
679        self.description = dto.description;
680        self.notes = dto.notes;
681        self.currency = dto.currency;
682        self.total_debit = dto.total_debit;
683        self.total_credit = dto.total_credit;
684        self.line_count = dto.line_count;
685        self.source = dto.source;
686        self.source_type = dto.source_type;
687        self.source_id = dto.source_id;
688        self.source_reference = dto.source_reference;
689        self.is_reversed = dto.is_reversed;
690        self.reversed_by_id = dto.reversed_by_id;
691        self.reversed_at = dto.reversed_at;
692        self.reversal_reason = dto.reversal_reason;
693        self.reverses_id = dto.reverses_id;
694        self.is_reversing = dto.is_reversing;
695        self.auto_reverse = dto.auto_reverse;
696        self.auto_reverse_date = dto.auto_reverse_date;
697        self.status = dto.status;
698        self.requires_approval = dto.requires_approval;
699        self.approval_threshold = dto.approval_threshold;
700        self.submitted_at = dto.submitted_at;
701        self.submitted_by = dto.submitted_by;
702        self.approved_at = dto.approved_at;
703        self.approved_by = dto.approved_by;
704        self.rejected_at = dto.rejected_at;
705        self.rejected_by = dto.rejected_by;
706        self.rejection_reason = dto.rejection_reason;
707        self.posted_at = dto.posted_at;
708        self.posted_by = dto.posted_by;
709        self.is_voided = dto.is_voided;
710        self.voided_at = dto.voided_at;
711        self.voided_by = dto.voided_by;
712        self.void_reason = dto.void_reason;
713        self.attachments = dto.attachments;
714        Ok(self)
715    }
716}
717
718// =============================================================================
719// Custom DTOs
720// =============================================================================
721
722// Add custom DTOs specific to Journal here.
723// This section will be preserved during regeneration.
724// >>> END CUSTOM DTOs
725