Skip to main content

koprogo_api/infrastructure/database/repositories/
ticket_repository_impl.rs

1use crate::application::ports::TicketRepository;
2use crate::domain::entities::{Ticket, TicketCategory, TicketPriority, TicketStatus};
3use async_trait::async_trait;
4use sqlx::PgPool;
5use uuid::Uuid;
6
7/// PostgreSQL implementation of TicketRepository
8pub struct PostgresTicketRepository {
9    pool: PgPool,
10}
11
12impl PostgresTicketRepository {
13    pub fn new(pool: PgPool) -> Self {
14        Self { pool }
15    }
16
17    /// Convert TicketCategory enum to database string
18    fn category_to_db(category: &TicketCategory) -> &'static str {
19        match category {
20            TicketCategory::Plumbing => "Plumbing",
21            TicketCategory::Electrical => "Electrical",
22            TicketCategory::Heating => "Heating",
23            TicketCategory::CommonAreas => "CommonAreas",
24            TicketCategory::Elevator => "Elevator",
25            TicketCategory::Security => "Security",
26            TicketCategory::Cleaning => "Cleaning",
27            TicketCategory::Landscaping => "Landscaping",
28            TicketCategory::Other => "Other",
29        }
30    }
31
32    /// Convert database string to TicketCategory enum
33    fn category_from_db(s: &str) -> Result<TicketCategory, String> {
34        match s {
35            "Plumbing" => Ok(TicketCategory::Plumbing),
36            "Electrical" => Ok(TicketCategory::Electrical),
37            "Heating" => Ok(TicketCategory::Heating),
38            "CommonAreas" => Ok(TicketCategory::CommonAreas),
39            "Elevator" => Ok(TicketCategory::Elevator),
40            "Security" => Ok(TicketCategory::Security),
41            "Cleaning" => Ok(TicketCategory::Cleaning),
42            "Landscaping" => Ok(TicketCategory::Landscaping),
43            "Other" => Ok(TicketCategory::Other),
44            _ => Err(format!("Invalid ticket category: {}", s)),
45        }
46    }
47
48    /// Convert TicketPriority enum to database string
49    fn priority_to_db(priority: &TicketPriority) -> &'static str {
50        match priority {
51            TicketPriority::Low => "Low",
52            TicketPriority::Medium => "Medium",
53            TicketPriority::High => "High",
54            TicketPriority::Critical => "Critical",
55        }
56    }
57
58    /// Convert database string to TicketPriority enum
59    fn priority_from_db(s: &str) -> Result<TicketPriority, String> {
60        match s {
61            "Low" => Ok(TicketPriority::Low),
62            "Medium" => Ok(TicketPriority::Medium),
63            "High" => Ok(TicketPriority::High),
64            "Critical" => Ok(TicketPriority::Critical),
65            _ => Err(format!("Invalid ticket priority: {}", s)),
66        }
67    }
68
69    /// Convert TicketStatus enum to database string
70    fn status_to_db(status: &TicketStatus) -> &'static str {
71        match status {
72            TicketStatus::Open => "Open",
73            TicketStatus::InProgress => "InProgress",
74            TicketStatus::Resolved => "Resolved",
75            TicketStatus::Closed => "Closed",
76            TicketStatus::Cancelled => "Cancelled",
77        }
78    }
79
80    /// Convert database string to TicketStatus enum
81    fn status_from_db(s: &str) -> Result<TicketStatus, String> {
82        match s {
83            "Open" => Ok(TicketStatus::Open),
84            "InProgress" => Ok(TicketStatus::InProgress),
85            "Resolved" => Ok(TicketStatus::Resolved),
86            "Closed" => Ok(TicketStatus::Closed),
87            "Cancelled" => Ok(TicketStatus::Cancelled),
88            _ => Err(format!("Invalid ticket status: {}", s)),
89        }
90    }
91}
92
93#[async_trait]
94impl TicketRepository for PostgresTicketRepository {
95    async fn create(&self, ticket: &Ticket) -> Result<Ticket, String> {
96        let category_str = Self::category_to_db(&ticket.category);
97        let priority_str = Self::priority_to_db(&ticket.priority);
98        let status_str = Self::status_to_db(&ticket.status);
99
100        let row = sqlx::query!(
101            r#"
102            INSERT INTO tickets (
103                id, organization_id, building_id, unit_id, created_by, assigned_to,
104                title, description, category, priority, status, resolution_notes,
105                created_at, updated_at, resolved_at, closed_at, work_order_sent_at
106            )
107            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
108            RETURNING id, organization_id, building_id, unit_id, created_by, assigned_to,
109                      title, description, category, priority, status, resolution_notes,
110                      created_at, updated_at, resolved_at, closed_at, work_order_sent_at
111            "#,
112            ticket.id,
113            ticket.organization_id,
114            ticket.building_id,
115            ticket.unit_id,
116            ticket.created_by,
117            ticket.assigned_to,
118            ticket.title,
119            ticket.description,
120            category_str,
121            priority_str,
122            status_str,
123            ticket.resolution_notes,
124            ticket.created_at,
125            ticket.updated_at,
126            ticket.resolved_at,
127            ticket.closed_at,
128            ticket.work_order_sent_at
129        )
130        .fetch_one(&self.pool)
131        .await
132        .map_err(|e| format!("Database error creating ticket: {}", e))?;
133
134        Ok(Ticket {
135            id: row.id,
136            organization_id: row.organization_id,
137            building_id: row.building_id,
138            unit_id: row.unit_id,
139            created_by: row.created_by,
140            assigned_to: row.assigned_to,
141            title: row.title,
142            description: row.description,
143            category: Self::category_from_db(&row.category)?,
144            priority: Self::priority_from_db(&row.priority)?,
145            status: Self::status_from_db(&row.status)?,
146            resolution_notes: row.resolution_notes,
147            created_at: row.created_at,
148            updated_at: row.updated_at,
149            resolved_at: row.resolved_at,
150            closed_at: row.closed_at,
151            work_order_sent_at: row.work_order_sent_at,
152            // Story 3.6 (FR31) — Phase A: repository keeps the legacy SQL
153            // surface untouched (no SQLx-cache breakage). Newly-added DB
154            // columns carry their migration defaults (`request`, empty arrays);
155            // the entity falls back to the same defaults on read-back here.
156            // Full read/write of complaint fields lands with the repo refresh
157            // tracked in cluster #555 (and the Phase B FE pulls them then).
158            kind: crate::domain::entities::TicketKind::Request,
159            severity: None,
160            incident_date: None,
161            evidence_attachments: Vec::new(),
162            witnesses: Vec::new(),
163            // Story 3.7 — SLA columns added in 20260605050000; repo refresh
164            // tracked in #555. Read-back uses None until then.
165            sla_due_at: None,
166            sla_escalated_at: None,
167        })
168    }
169
170    async fn find_by_id(&self, id: Uuid) -> Result<Option<Ticket>, String> {
171        let row = sqlx::query!(
172            r#"
173            SELECT id, organization_id, building_id, unit_id, created_by, assigned_to,
174                   title, description, category, priority, status, resolution_notes,
175                   created_at, updated_at, resolved_at, closed_at, work_order_sent_at
176            FROM tickets
177            WHERE id = $1
178            "#,
179            id
180        )
181        .fetch_optional(&self.pool)
182        .await
183        .map_err(|e| format!("Database error finding ticket: {}", e))?;
184
185        match row {
186            Some(r) => Ok(Some(Ticket {
187                id: r.id,
188                organization_id: r.organization_id,
189                building_id: r.building_id,
190                unit_id: r.unit_id,
191                created_by: r.created_by,
192                assigned_to: r.assigned_to,
193                title: r.title,
194                description: r.description,
195                category: Self::category_from_db(&r.category)?,
196                priority: Self::priority_from_db(&r.priority)?,
197                status: Self::status_from_db(&r.status)?,
198                resolution_notes: r.resolution_notes,
199                created_at: r.created_at,
200                updated_at: r.updated_at,
201                resolved_at: r.resolved_at,
202                closed_at: r.closed_at,
203                work_order_sent_at: r.work_order_sent_at,
204                // Story 3.6 (FR31) — repo not yet wired for these columns
205                // (Phase A keeps SQLx cache untouched). DB defaults apply on
206                // INSERT; read-back falls back to entity defaults until the
207                // repo refresh tracked in #555.
208                kind: crate::domain::entities::TicketKind::Request,
209                severity: None,
210                incident_date: None,
211                evidence_attachments: Vec::new(),
212                witnesses: Vec::new(),
213                // Story 3.7 — SLA defaults; repo refresh tracked in #555.
214                sla_due_at: None,
215                sla_escalated_at: None,
216            })),
217            None => Ok(None),
218        }
219    }
220
221    async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Ticket>, String> {
222        let rows = sqlx::query!(
223            r#"
224            SELECT id, organization_id, building_id, unit_id, created_by, assigned_to,
225                   title, description, category, priority, status, resolution_notes,
226                   created_at, updated_at, resolved_at, closed_at, work_order_sent_at
227            FROM tickets
228            WHERE building_id = $1
229            ORDER BY created_at DESC
230            "#,
231            building_id
232        )
233        .fetch_all(&self.pool)
234        .await
235        .map_err(|e| format!("Database error finding tickets by building: {}", e))?;
236
237        rows.into_iter()
238            .map(|r| {
239                Ok(Ticket {
240                    id: r.id,
241                    organization_id: r.organization_id,
242                    building_id: r.building_id,
243                    unit_id: r.unit_id,
244                    created_by: r.created_by,
245                    assigned_to: r.assigned_to,
246                    title: r.title,
247                    description: r.description,
248                    category: Self::category_from_db(&r.category)?,
249                    priority: Self::priority_from_db(&r.priority)?,
250                    status: Self::status_from_db(&r.status)?,
251                    resolution_notes: r.resolution_notes,
252                    created_at: r.created_at,
253                    updated_at: r.updated_at,
254                    resolved_at: r.resolved_at,
255                    closed_at: r.closed_at,
256                    work_order_sent_at: r.work_order_sent_at,
257                    // Story 3.6 (FR31) — see same comment in `find_by_id`.
258                    kind: crate::domain::entities::TicketKind::Request,
259                    severity: None,
260                    incident_date: None,
261                    evidence_attachments: Vec::new(),
262                    witnesses: Vec::new(),
263                    // Story 3.7 — SLA defaults; repo refresh tracked in #555.
264                    sla_due_at: None,
265                    sla_escalated_at: None,
266                })
267            })
268            .collect()
269    }
270
271    async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<Ticket>, String> {
272        let rows = sqlx::query!(
273            r#"
274            SELECT id, organization_id, building_id, unit_id, created_by, assigned_to,
275                   title, description, category, priority, status, resolution_notes,
276                   created_at, updated_at, resolved_at, closed_at, work_order_sent_at
277            FROM tickets
278            WHERE organization_id = $1
279            ORDER BY created_at DESC
280            "#,
281            organization_id
282        )
283        .fetch_all(&self.pool)
284        .await
285        .map_err(|e| format!("Database error finding tickets by organization: {}", e))?;
286
287        rows.into_iter()
288            .map(|r| {
289                Ok(Ticket {
290                    id: r.id,
291                    organization_id: r.organization_id,
292                    building_id: r.building_id,
293                    unit_id: r.unit_id,
294                    created_by: r.created_by,
295                    assigned_to: r.assigned_to,
296                    title: r.title,
297                    description: r.description,
298                    category: Self::category_from_db(&r.category)?,
299                    priority: Self::priority_from_db(&r.priority)?,
300                    status: Self::status_from_db(&r.status)?,
301                    resolution_notes: r.resolution_notes,
302                    created_at: r.created_at,
303                    updated_at: r.updated_at,
304                    resolved_at: r.resolved_at,
305                    closed_at: r.closed_at,
306                    work_order_sent_at: r.work_order_sent_at,
307                    // Story 3.6 (FR31) — see same comment in `find_by_id`.
308                    kind: crate::domain::entities::TicketKind::Request,
309                    severity: None,
310                    incident_date: None,
311                    evidence_attachments: Vec::new(),
312                    witnesses: Vec::new(),
313                    // Story 3.7 — SLA defaults; repo refresh tracked in #555.
314                    sla_due_at: None,
315                    sla_escalated_at: None,
316                })
317            })
318            .collect()
319    }
320
321    async fn find_by_created_by(&self, created_by: Uuid) -> Result<Vec<Ticket>, String> {
322        let rows = sqlx::query!(
323            r#"
324            SELECT id, organization_id, building_id, unit_id, created_by, assigned_to,
325                   title, description, category, priority, status, resolution_notes,
326                   created_at, updated_at, resolved_at, closed_at, work_order_sent_at
327            FROM tickets
328            WHERE created_by = $1
329            ORDER BY created_at DESC
330            "#,
331            created_by
332        )
333        .fetch_all(&self.pool)
334        .await
335        .map_err(|e| format!("Database error finding tickets by creator: {}", e))?;
336
337        rows.into_iter()
338            .map(|r| {
339                Ok(Ticket {
340                    id: r.id,
341                    organization_id: r.organization_id,
342                    building_id: r.building_id,
343                    unit_id: r.unit_id,
344                    created_by: r.created_by,
345                    assigned_to: r.assigned_to,
346                    title: r.title,
347                    description: r.description,
348                    category: Self::category_from_db(&r.category)?,
349                    priority: Self::priority_from_db(&r.priority)?,
350                    status: Self::status_from_db(&r.status)?,
351                    resolution_notes: r.resolution_notes,
352                    created_at: r.created_at,
353                    updated_at: r.updated_at,
354                    resolved_at: r.resolved_at,
355                    closed_at: r.closed_at,
356                    work_order_sent_at: r.work_order_sent_at,
357                    // Story 3.6 (FR31) — see same comment in `find_by_id`.
358                    kind: crate::domain::entities::TicketKind::Request,
359                    severity: None,
360                    incident_date: None,
361                    evidence_attachments: Vec::new(),
362                    witnesses: Vec::new(),
363                    // Story 3.7 — SLA defaults; repo refresh tracked in #555.
364                    sla_due_at: None,
365                    sla_escalated_at: None,
366                })
367            })
368            .collect()
369    }
370
371    async fn find_by_assigned_to(&self, assigned_to: Uuid) -> Result<Vec<Ticket>, String> {
372        let rows = sqlx::query!(
373            r#"
374            SELECT id, organization_id, building_id, unit_id, created_by, assigned_to,
375                   title, description, category, priority, status, resolution_notes,
376                   created_at, updated_at, resolved_at, closed_at, work_order_sent_at
377            FROM tickets
378            WHERE assigned_to = $1
379            ORDER BY created_at DESC
380            "#,
381            assigned_to
382        )
383        .fetch_all(&self.pool)
384        .await
385        .map_err(|e| format!("Database error finding tickets by assignee: {}", e))?;
386
387        rows.into_iter()
388            .map(|r| {
389                Ok(Ticket {
390                    id: r.id,
391                    organization_id: r.organization_id,
392                    building_id: r.building_id,
393                    unit_id: r.unit_id,
394                    created_by: r.created_by,
395                    assigned_to: r.assigned_to,
396                    title: r.title,
397                    description: r.description,
398                    category: Self::category_from_db(&r.category)?,
399                    priority: Self::priority_from_db(&r.priority)?,
400                    status: Self::status_from_db(&r.status)?,
401                    resolution_notes: r.resolution_notes,
402                    created_at: r.created_at,
403                    updated_at: r.updated_at,
404                    resolved_at: r.resolved_at,
405                    closed_at: r.closed_at,
406                    work_order_sent_at: r.work_order_sent_at,
407                    // Story 3.6 (FR31) — see same comment in `find_by_id`.
408                    kind: crate::domain::entities::TicketKind::Request,
409                    severity: None,
410                    incident_date: None,
411                    evidence_attachments: Vec::new(),
412                    witnesses: Vec::new(),
413                    // Story 3.7 — SLA defaults; repo refresh tracked in #555.
414                    sla_due_at: None,
415                    sla_escalated_at: None,
416                })
417            })
418            .collect()
419    }
420
421    async fn find_by_status(
422        &self,
423        building_id: Uuid,
424        status: TicketStatus,
425    ) -> Result<Vec<Ticket>, String> {
426        let status_str = Self::status_to_db(&status);
427
428        let rows = sqlx::query!(
429            r#"
430            SELECT id, organization_id, building_id, unit_id, created_by, assigned_to,
431                   title, description, category, priority, status, resolution_notes,
432                   created_at, updated_at, resolved_at, closed_at, work_order_sent_at
433            FROM tickets
434            WHERE building_id = $1 AND status = $2
435            ORDER BY created_at DESC
436            "#,
437            building_id,
438            status_str
439        )
440        .fetch_all(&self.pool)
441        .await
442        .map_err(|e| format!("Database error finding tickets by status: {}", e))?;
443
444        rows.into_iter()
445            .map(|r| {
446                Ok(Ticket {
447                    id: r.id,
448                    organization_id: r.organization_id,
449                    building_id: r.building_id,
450                    unit_id: r.unit_id,
451                    created_by: r.created_by,
452                    assigned_to: r.assigned_to,
453                    title: r.title,
454                    description: r.description,
455                    category: Self::category_from_db(&r.category)?,
456                    priority: Self::priority_from_db(&r.priority)?,
457                    status: Self::status_from_db(&r.status)?,
458                    resolution_notes: r.resolution_notes,
459                    created_at: r.created_at,
460                    updated_at: r.updated_at,
461                    resolved_at: r.resolved_at,
462                    closed_at: r.closed_at,
463                    work_order_sent_at: r.work_order_sent_at,
464                    // Story 3.6 (FR31) — see same comment in `find_by_id`.
465                    kind: crate::domain::entities::TicketKind::Request,
466                    severity: None,
467                    incident_date: None,
468                    evidence_attachments: Vec::new(),
469                    witnesses: Vec::new(),
470                    // Story 3.7 — SLA defaults; repo refresh tracked in #555.
471                    sla_due_at: None,
472                    sla_escalated_at: None,
473                })
474            })
475            .collect()
476    }
477
478    async fn update(&self, ticket: &Ticket) -> Result<Ticket, String> {
479        let category_str = Self::category_to_db(&ticket.category);
480        let priority_str = Self::priority_to_db(&ticket.priority);
481        let status_str = Self::status_to_db(&ticket.status);
482
483        let row = sqlx::query!(
484            r#"
485            UPDATE tickets
486            SET organization_id = $2,
487                building_id = $3,
488                unit_id = $4,
489                created_by = $5,
490                assigned_to = $6,
491                title = $7,
492                description = $8,
493                category = $9,
494                priority = $10,
495                status = $11,
496                resolution_notes = $12,
497                updated_at = $13,
498                resolved_at = $14,
499                closed_at = $15,
500                work_order_sent_at = $16
501            WHERE id = $1
502            RETURNING id, organization_id, building_id, unit_id, created_by, assigned_to,
503                      title, description, category, priority, status, resolution_notes,
504                      created_at, updated_at, resolved_at, closed_at, work_order_sent_at
505            "#,
506            ticket.id,
507            ticket.organization_id,
508            ticket.building_id,
509            ticket.unit_id,
510            ticket.created_by,
511            ticket.assigned_to,
512            ticket.title,
513            ticket.description,
514            category_str,
515            priority_str,
516            status_str,
517            ticket.resolution_notes,
518            ticket.updated_at,
519            ticket.resolved_at,
520            ticket.closed_at,
521            ticket.work_order_sent_at
522        )
523        .fetch_one(&self.pool)
524        .await
525        .map_err(|e| format!("Database error updating ticket: {}", e))?;
526
527        Ok(Ticket {
528            id: row.id,
529            organization_id: row.organization_id,
530            building_id: row.building_id,
531            unit_id: row.unit_id,
532            created_by: row.created_by,
533            assigned_to: row.assigned_to,
534            title: row.title,
535            description: row.description,
536            category: Self::category_from_db(&row.category)?,
537            priority: Self::priority_from_db(&row.priority)?,
538            status: Self::status_from_db(&row.status)?,
539            resolution_notes: row.resolution_notes,
540            created_at: row.created_at,
541            updated_at: row.updated_at,
542            resolved_at: row.resolved_at,
543            closed_at: row.closed_at,
544            work_order_sent_at: row.work_order_sent_at,
545            // Story 3.6 (FR31) — Phase A: repository keeps the legacy SQL
546            // surface untouched (no SQLx-cache breakage). Newly-added DB
547            // columns carry their migration defaults (`request`, empty arrays);
548            // the entity falls back to the same defaults on read-back here.
549            // Full read/write of complaint fields lands with the repo refresh
550            // tracked in cluster #555 (and the Phase B FE pulls them then).
551            kind: crate::domain::entities::TicketKind::Request,
552            severity: None,
553            incident_date: None,
554            evidence_attachments: Vec::new(),
555            witnesses: Vec::new(),
556            // Story 3.7 — SLA columns added in 20260605050000; repo refresh
557            // tracked in #555. Read-back uses None until then.
558            sla_due_at: None,
559            sla_escalated_at: None,
560        })
561    }
562
563    async fn delete(&self, id: Uuid) -> Result<bool, String> {
564        let result = sqlx::query!(
565            r#"
566            DELETE FROM tickets
567            WHERE id = $1
568            "#,
569            id
570        )
571        .execute(&self.pool)
572        .await
573        .map_err(|e| format!("Database error deleting ticket: {}", e))?;
574
575        Ok(result.rows_affected() > 0)
576    }
577
578    async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
579        let row = sqlx::query!(
580            r#"
581            SELECT COUNT(*) as count
582            FROM tickets
583            WHERE building_id = $1
584            "#,
585            building_id
586        )
587        .fetch_one(&self.pool)
588        .await
589        .map_err(|e| format!("Database error counting tickets: {}", e))?;
590
591        Ok(row.count.unwrap_or(0))
592    }
593
594    async fn count_by_status(
595        &self,
596        building_id: Uuid,
597        status: TicketStatus,
598    ) -> Result<i64, String> {
599        let status_str = Self::status_to_db(&status);
600
601        let row = sqlx::query!(
602            r#"
603            SELECT COUNT(*) as count
604            FROM tickets
605            WHERE building_id = $1 AND status = $2
606            "#,
607            building_id,
608            status_str
609        )
610        .fetch_one(&self.pool)
611        .await
612        .map_err(|e| format!("Database error counting tickets by status: {}", e))?;
613
614        Ok(row.count.unwrap_or(0))
615    }
616
617    async fn count_by_organization(&self, organization_id: Uuid) -> Result<i64, String> {
618        let count: (i64,) =
619            sqlx::query_as("SELECT COUNT(*) FROM tickets WHERE organization_id = $1")
620                .bind(organization_id)
621                .fetch_one(&self.pool)
622                .await
623                .map_err(|e| format!("Database error counting tickets by organization: {}", e))?;
624
625        Ok(count.0)
626    }
627
628    async fn count_by_organization_and_status(
629        &self,
630        organization_id: Uuid,
631        status: TicketStatus,
632    ) -> Result<i64, String> {
633        let status_str = Self::status_to_db(&status);
634
635        let count: (i64,) = sqlx::query_as(
636            "SELECT COUNT(*) FROM tickets WHERE organization_id = $1 AND status = $2",
637        )
638        .bind(organization_id)
639        .bind(status_str)
640        .fetch_one(&self.pool)
641        .await
642        .map_err(|e| format!("Database error counting tickets by org and status: {}", e))?;
643
644        Ok(count.0)
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651
652    #[test]
653    fn test_category_conversion() {
654        assert_eq!(
655            PostgresTicketRepository::category_to_db(&TicketCategory::Plumbing),
656            "Plumbing"
657        );
658        assert_eq!(
659            PostgresTicketRepository::category_from_db("Electrical").unwrap(),
660            TicketCategory::Electrical
661        );
662    }
663
664    #[test]
665    fn test_priority_conversion() {
666        assert_eq!(
667            PostgresTicketRepository::priority_to_db(&TicketPriority::Critical),
668            "Critical"
669        );
670        assert_eq!(
671            PostgresTicketRepository::priority_from_db("Low").unwrap(),
672            TicketPriority::Low
673        );
674    }
675
676    #[test]
677    fn test_status_conversion() {
678        assert_eq!(
679            PostgresTicketRepository::status_to_db(&TicketStatus::InProgress),
680            "InProgress"
681        );
682        assert_eq!(
683            PostgresTicketRepository::status_from_db("Resolved").unwrap(),
684            TicketStatus::Resolved
685        );
686    }
687
688    #[test]
689    fn test_invalid_category() {
690        assert!(PostgresTicketRepository::category_from_db("Invalid").is_err());
691    }
692
693    #[test]
694    fn test_invalid_priority() {
695        assert!(PostgresTicketRepository::priority_from_db("Invalid").is_err());
696    }
697
698    #[test]
699    fn test_invalid_status() {
700        assert!(PostgresTicketRepository::status_from_db("Invalid").is_err());
701    }
702}