# Sag Module API Documentation ## Overview This document provides comprehensive API documentation for the Sag (Case) module endpoints. All endpoints follow RESTful conventions and return JSON responses. **Base URL**: `http://localhost:8001/api/v1` --- ## Cases CRUD ### List All Cases Retrieve a list of all cases with optional filtering. **Endpoint**: `GET /cases` **Query Parameters**: - `status` (optional): Filter by status (`åben`, `lukket`) - `customer_id` (optional): Filter by customer ID **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/cases?status=åben&customer_id=123" ``` **Response**: `200 OK` ```json [ { "id": 1, "titel": "Server maintenance", "beskrivelse": "Regular server maintenance", "template_key": null, "status": "åben", "customer_id": 123, "ansvarlig_bruger_id": 5, "created_by_user_id": 5, "deadline": "2026-02-15T10:00:00", "created_at": "2026-01-15T09:00:00", "updated_at": "2026-01-15T09:00:00", "deleted_at": null } ] ``` **Error Responses**: - `500 Internal Server Error` - Database connection error --- ### Create New Case Create a new case in the system. **Endpoint**: `POST /cases` **Request Body**: ```json { "titel": "Network issue troubleshooting", "beskrivelse": "Customer reports intermittent connection drops", "template_key": "support_ticket", "status": "åben", "customer_id": 456, "ansvarlig_bruger_id": 3, "created_by_user_id": 3, "deadline": "2026-02-01T17:00:00" } ``` **Request Example**: ```bash curl -X POST "http://localhost:8001/api/v1/cases" \ -H "Content-Type: application/json" \ -d '{ "titel": "Network issue troubleshooting", "beskrivelse": "Customer reports intermittent connection drops", "status": "åben", "customer_id": 456 }' ``` **Response**: `201 Created` ```json { "id": 42, "titel": "Network issue troubleshooting", "beskrivelse": "Customer reports intermittent connection drops", "template_key": "support_ticket", "status": "åben", "customer_id": 456, "ansvarlig_bruger_id": 3, "created_by_user_id": 3, "deadline": "2026-02-01T17:00:00", "created_at": "2026-01-30T14:23:00", "updated_at": "2026-01-30T14:23:00", "deleted_at": null } ``` **Error Responses**: - `400 Bad Request` - Missing required fields - `500 Internal Server Error` - Failed to create case --- ### Get Case by ID Retrieve a specific case by its ID. **Endpoint**: `GET /cases/{id}` **Path Parameters**: - `id` (required): Case ID **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/cases/42" ``` **Response**: `200 OK` ```json { "id": 42, "titel": "Network issue troubleshooting", "beskrivelse": "Customer reports intermittent connection drops", "template_key": "support_ticket", "status": "åben", "customer_id": 456, "ansvarlig_bruger_id": 3, "created_by_user_id": 3, "deadline": "2026-02-01T17:00:00", "created_at": "2026-01-30T14:23:00", "updated_at": "2026-01-30T14:23:00", "deleted_at": null } ``` **Error Responses**: - `404 Not Found` - Case not found or deleted --- ### Update Case Update an existing case. Only provided fields will be updated. **Endpoint**: `PATCH /cases/{id}` **Path Parameters**: - `id` (required): Case ID **Request Body** (all fields optional): ```json { "titel": "Updated title", "beskrivelse": "Updated description", "status": "lukket", "ansvarlig_bruger_id": 7, "deadline": "2026-02-10T12:00:00" } ``` **Request Example**: ```bash curl -X PATCH "http://localhost:8001/api/v1/cases/42" \ -H "Content-Type: application/json" \ -d '{"status": "lukket"}' ``` **Response**: `200 OK` ```json { "id": 42, "titel": "Network issue troubleshooting", "beskrivelse": "Customer reports intermittent connection drops", "template_key": "support_ticket", "status": "lukket", "customer_id": 456, "ansvarlig_bruger_id": 3, "created_by_user_id": 3, "deadline": "2026-02-01T17:00:00", "created_at": "2026-01-30T14:23:00", "updated_at": "2026-01-30T15:45:00", "deleted_at": null } ``` **Error Responses**: - `404 Not Found` - Case not found or deleted - `400 Bad Request` - Invalid field values --- ### Delete Case (Soft Delete) Soft-delete a case. The case is not removed from the database but marked as deleted. **Endpoint**: `DELETE /cases/{id}` **Path Parameters**: - `id` (required): Case ID **Request Example**: ```bash curl -X DELETE "http://localhost:8001/api/v1/cases/42" ``` **Response**: `200 OK` ```json { "id": 42, "titel": "Network issue troubleshooting", "beskrivelse": "Customer reports intermittent connection drops", "template_key": "support_ticket", "status": "lukket", "customer_id": 456, "ansvarlig_bruger_id": 3, "created_by_user_id": 3, "deadline": "2026-02-01T17:00:00", "created_at": "2026-01-30T14:23:00", "updated_at": "2026-01-30T15:45:00", "deleted_at": "2026-01-30T16:00:00" } ``` **Error Responses**: - `404 Not Found` - Case not found or already deleted --- ## Tags ### List Tags for Case Retrieve all tags associated with a case. **Endpoint**: `GET /cases/{id}/tags` **Path Parameters**: - `id` (required): Case ID **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/cases/42/tags" ``` **Response**: `200 OK` ```json [ { "id": 10, "sag_id": 42, "tag_navn": "urgent", "state": "open", "closed_at": null, "created_at": "2026-01-30T14:25:00", "deleted_at": null }, { "id": 11, "sag_id": 42, "tag_navn": "network", "state": "closed", "closed_at": "2026-01-30T15:30:00", "created_at": "2026-01-30T14:25:00", "deleted_at": null } ] ``` **Error Responses**: - `500 Internal Server Error` - Database error --- ### Add Tag to Case Add a new tag to a case. **Endpoint**: `POST /cases/{id}/tags` **Path Parameters**: - `id` (required): Case ID **Request Body**: ```json { "tag_navn": "high_priority", "state": "open" } ``` **Note**: `state` defaults to `"open"` if not provided. **Request Example**: ```bash curl -X POST "http://localhost:8001/api/v1/cases/42/tags" \ -H "Content-Type: application/json" \ -d '{"tag_navn": "high_priority"}' ``` **Response**: `201 Created` ```json { "id": 12, "sag_id": 42, "tag_navn": "high_priority", "state": "open", "closed_at": null, "created_at": "2026-01-30T16:10:00", "deleted_at": null } ``` **Error Responses**: - `400 Bad Request` - Missing tag_navn - `500 Internal Server Error` - Failed to create tag --- ### Update Tag State Change tag state between `open` and `closed`. Tags are never deleted, only closed when work completes. **Endpoint**: `PATCH /cases/{id}/tags/{tag_id}/state` **Path Parameters**: - `id` (required): Case ID - `tag_id` (required): Tag ID **Request Body**: ```json { "state": "closed" } ``` **Valid states**: `"open"` or `"closed"` **Request Example**: ```bash curl -X PATCH "http://localhost:8001/api/v1/cases/42/tags/12/state" \ -H "Content-Type: application/json" \ -d '{"state": "closed"}' ``` **Response**: `200 OK` ```json { "id": 12, "sag_id": 42, "tag_navn": "high_priority", "state": "closed", "closed_at": "2026-01-30T16:45:00", "created_at": "2026-01-30T16:10:00", "deleted_at": null } ``` **Error Responses**: - `400 Bad Request` - Invalid state value - `404 Not Found` - Tag not found or doesn't belong to case --- ### Delete Tag (Soft Delete) Soft-delete a tag from a case. **Endpoint**: `DELETE /cases/{id}/tags/{tag_id}` **Path Parameters**: - `id` (required): Case ID - `tag_id` (required): Tag ID **Request Example**: ```bash curl -X DELETE "http://localhost:8001/api/v1/cases/42/tags/12" ``` **Response**: `200 OK` ```json { "id": 12, "sag_id": 42, "tag_navn": "high_priority", "state": "closed", "closed_at": "2026-01-30T16:45:00", "created_at": "2026-01-30T16:10:00", "deleted_at": "2026-01-30T17:00:00" } ``` **Error Responses**: - `404 Not Found` - Tag not found or already deleted --- ## Relations ### List Case Relations Retrieve all relations for a specific case (both incoming and outgoing). **Endpoint**: `GET /cases/{id}/relations` **Path Parameters**: - `id` (required): Case ID **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/cases/42/relations" ``` **Response**: `200 OK` ```json [ { "id": 5, "kilde_sag_id": 42, "målsag_id": 55, "relationstype": "blokkerer", "kilde_titel": "Network issue troubleshooting", "mål_titel": "Deploy new firewall", "created_at": "2026-01-30T14:30:00", "deleted_at": null }, { "id": 6, "kilde_sag_id": 30, "målsag_id": 42, "relationstype": "afhænger af", "kilde_titel": "Server upgrade", "mål_titel": "Network issue troubleshooting", "created_at": "2026-01-30T15:00:00", "deleted_at": null } ] ``` **Error Responses**: - `500 Internal Server Error` - Database error --- ### Create Relation Create a new relation between two cases. **Endpoint**: `POST /cases/{id}/relations` **Path Parameters**: - `id` (required): Source case ID (kilde_sag_id) **Request Body**: ```json { "målsag_id": 55, "relationstype": "blokkerer" } ``` **Valid relation types**: - `relateret` - General relation - `afhænger af` - Source depends on target - `blokkerer` - Source blocks target - `duplikat` - Source duplicates target **Request Example**: ```bash curl -X POST "http://localhost:8001/api/v1/cases/42/relations" \ -H "Content-Type: application/json" \ -d '{"målsag_id": 55, "relationstype": "blokkerer"}' ``` **Response**: `201 Created` ```json { "id": 7, "kilde_sag_id": 42, "målsag_id": 55, "relationstype": "blokkerer", "created_at": "2026-01-30T17:15:00", "deleted_at": null } ``` **Error Responses**: - `400 Bad Request` - Missing fields or self-reference - `404 Not Found` - Target case not found - `500 Internal Server Error` - Failed to create relation --- ### Delete Relation (Soft Delete) Soft-delete a relation between cases. **Endpoint**: `DELETE /cases/{id}/relations/{relation_id}` **Path Parameters**: - `id` (required): Case ID - `relation_id` (required): Relation ID **Request Example**: ```bash curl -X DELETE "http://localhost:8001/api/v1/cases/42/relations/7" ``` **Response**: `200 OK` ```json { "id": 7, "kilde_sag_id": 42, "målsag_id": 55, "relationstype": "blokkerer", "created_at": "2026-01-30T17:15:00", "deleted_at": "2026-01-30T17:45:00" } ``` **Error Responses**: - `404 Not Found` - Relation not found or already deleted --- ## Contacts ### List Case Contacts Retrieve all contacts linked to a case. **Endpoint**: `GET /cases/{id}/contacts` **Path Parameters**: - `id` (required): Case ID **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/cases/42/contacts" ``` **Response**: `200 OK` ```json [ { "id": 3, "sag_id": 42, "contact_id": 15, "role": "Kontakt", "contact_name": "John Doe", "contact_email": "john.doe@example.com", "created_at": "2026-01-30T14:30:00", "deleted_at": null } ] ``` **Error Responses**: - `500 Internal Server Error` - Database error --- ### Add Contact to Case Link a contact to a case. **Endpoint**: `POST /cases/{id}/contacts` **Path Parameters**: - `id` (required): Case ID **Request Body**: ```json { "contact_id": 15, "role": "Primary Contact" } ``` **Note**: `role` defaults to `"Kontakt"` if not provided. **Request Example**: ```bash curl -X POST "http://localhost:8001/api/v1/cases/42/contacts" \ -H "Content-Type: application/json" \ -d '{"contact_id": 15, "role": "Primary Contact"}' ``` **Response**: `201 Created` ```json { "id": 4, "sag_id": 42, "contact_id": 15, "role": "Primary Contact", "created_at": "2026-01-30T18:00:00", "deleted_at": null } ``` **Error Responses**: - `400 Bad Request` - Missing contact_id or contact already linked - `500 Internal Server Error` - Failed to add contact --- ### Remove Contact from Case Unlink a contact from a case. **Endpoint**: `DELETE /cases/{id}/contacts/{contact_id}` **Path Parameters**: - `id` (required): Case ID - `contact_id` (required): Contact ID **Request Example**: ```bash curl -X DELETE "http://localhost:8001/api/v1/cases/42/contacts/15" ``` **Response**: `200 OK` ```json { "id": 4, "sag_id": 42, "contact_id": 15, "role": "Primary Contact", "created_at": "2026-01-30T18:00:00", "deleted_at": "2026-01-30T18:30:00" } ``` **Error Responses**: - `404 Not Found` - Contact not linked to case - `500 Internal Server Error` - Failed to remove contact --- ## Customers ### List Case Customers Retrieve all customers linked to a case. **Endpoint**: `GET /cases/{id}/customers` **Path Parameters**: - `id` (required): Case ID **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/cases/42/customers" ``` **Response**: `200 OK` ```json [ { "id": 2, "sag_id": 42, "customer_id": 456, "role": "Kunde", "customer_name": "Acme Corporation", "customer_email": "contact@acme.com", "created_at": "2026-01-30T14:30:00", "deleted_at": null } ] ``` **Error Responses**: - `500 Internal Server Error` - Database error --- ### Add Customer to Case Link a customer to a case. **Endpoint**: `POST /cases/{id}/customers` **Path Parameters**: - `id` (required): Case ID **Request Body**: ```json { "customer_id": 456, "role": "Main Customer" } ``` **Note**: `role` defaults to `"Kunde"` if not provided. **Request Example**: ```bash curl -X POST "http://localhost:8001/api/v1/cases/42/customers" \ -H "Content-Type: application/json" \ -d '{"customer_id": 456, "role": "Main Customer"}' ``` **Response**: `201 Created` ```json { "id": 3, "sag_id": 42, "customer_id": 456, "role": "Main Customer", "created_at": "2026-01-30T18:45:00", "deleted_at": null } ``` **Error Responses**: - `400 Bad Request` - Missing customer_id or customer already linked - `500 Internal Server Error` - Failed to add customer --- ### Remove Customer from Case Unlink a customer from a case. **Endpoint**: `DELETE /cases/{id}/customers/{customer_id}` **Path Parameters**: - `id` (required): Case ID - `customer_id` (required): Customer ID **Request Example**: ```bash curl -X DELETE "http://localhost:8001/api/v1/cases/42/customers/456" ``` **Response**: `200 OK` ```json { "id": 3, "sag_id": 42, "customer_id": 456, "role": "Main Customer", "created_at": "2026-01-30T18:45:00", "deleted_at": "2026-01-30T19:00:00" } ``` **Error Responses**: - `404 Not Found` - Customer not linked to case - `500 Internal Server Error` - Failed to remove customer --- ## Search ### Search Cases Search for cases by title or description. **Endpoint**: `GET /search/cases` **Query Parameters**: - `q` (required): Search query (minimum 2 characters) **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/search/cases?q=network" ``` **Response**: `200 OK` ```json [ { "id": 42, "titel": "Network issue troubleshooting", "status": "åben", "created_at": "2026-01-30T14:23:00" }, { "id": 55, "titel": "Deploy new firewall", "status": "åben", "created_at": "2026-01-28T10:00:00" } ] ``` **Error Responses**: - `500 Internal Server Error` - Search failed --- ### Search Contacts Search for contacts by name, email, or company. **Endpoint**: `GET /search/contacts` **Query Parameters**: - `q` (required): Search query (minimum 1 character) **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/search/contacts?q=john" ``` **Response**: `200 OK` ```json [ { "id": 15, "first_name": "John", "last_name": "Doe", "email": "john.doe@example.com", "user_company": "Acme Corp", "phone": "+45 12 34 56 78" } ] ``` **Note**: Limited to 20 results. Only active contacts are returned. **Error Responses**: - `400 Bad Request` - Query too short - `500 Internal Server Error` - Search failed --- ### Search Customers Search for customers by name, email, or CVR number. **Endpoint**: `GET /search/customers` **Query Parameters**: - `q` (required): Search query (minimum 1 character) **Request Example**: ```bash curl -X GET "http://localhost:8001/api/v1/search/customers?q=acme" ``` **Response**: `200 OK` ```json [ { "id": 456, "name": "Acme Corporation", "email": "contact@acme.com", "cvr_number": "12345678", "city": "Copenhagen" } ] ``` **Note**: Limited to 20 results. **Error Responses**: - `400 Bad Request` - Query too short - `500 Internal Server Error` - Search failed --- ## Bulk Operations ### Bulk Actions on Cases Perform bulk actions on multiple cases simultaneously. **Endpoint**: `POST /cases/bulk` **Request Body**: ```json { "case_ids": [42, 55, 67], "action": "update_status", "params": { "status": "lukket" } } ``` **Available Actions**: #### 1. Update Status ```json { "case_ids": [42, 55], "action": "update_status", "params": { "status": "lukket" } } ``` #### 2. Add Tag ```json { "case_ids": [42, 55, 67], "action": "add_tag", "params": { "tag_navn": "reviewed" } } ``` #### 3. Close All ```json { "case_ids": [42, 55, 67], "action": "close_all", "params": {} } ``` **Request Example**: ```bash curl -X POST "http://localhost:8001/api/v1/cases/bulk" \ -H "Content-Type: application/json" \ -d '{ "case_ids": [42, 55, 67], "action": "update_status", "params": {"status": "lukket"} }' ``` **Response**: `200 OK` ```json { "success": true, "affected_cases": 3, "action": "update_status" } ``` **Error Responses**: - `400 Bad Request` - Missing fields or invalid action - `500 Internal Server Error` - Bulk operation failed --- ## Common Error Responses All endpoints may return the following error codes: ### 400 Bad Request ```json { "detail": "Missing required field: titel" } ``` ### 404 Not Found ```json { "detail": "Case not found." } ``` ### 500 Internal Server Error ```json { "detail": "Database connection failed" } ``` --- ## Notes ### Soft Deletes All delete operations are **soft deletes**. Records are never removed from the database but marked with a `deleted_at` timestamp. This preserves data integrity and audit trails. ### Tag Philosophy Tags are **never hard-deleted**. Instead, they should be closed when work completes. This maintains a complete history of work done on a case. ### Relations Relations are **directional** (source → target) and **transitive**. The UI can derive parent/child views, but the database stores only directional links. ### Search Limits All search endpoints return a maximum of 20 results to prevent performance issues. Implement pagination if more results are needed. --- ## Architecture Principles The Sag module follows these principles: - **Simplicity** - One entity (Case), not many specialized types - **Flexibility** - Relations and tags express any structure - **Traceability** - Soft deletes preserve complete history - **Clarity** - Tags make workflow state explicit > "If you think you need a new table or workflow engine, you're probably wrong. Use relations and tags instead."