- Implement test script for new SAG module endpoints BE-003 (Tag State Management) and BE-004 (Bulk Operations). - Create test cases for creating, updating, and bulk operations on cases and tags. - Add a test for module deactivation to ensure data integrity is maintained. - Include setup and teardown for tests to clear database state before and after each test.
19 KiB
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:
curl -X GET "http://localhost:8001/api/v1/cases?status=åben&customer_id=123"
Response: 200 OK
[
{
"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:
{
"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:
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
{
"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 fields500 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:
curl -X GET "http://localhost:8001/api/v1/cases/42"
Response: 200 OK
{
"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):
{
"titel": "Updated title",
"beskrivelse": "Updated description",
"status": "lukket",
"ansvarlig_bruger_id": 7,
"deadline": "2026-02-10T12:00:00"
}
Request Example:
curl -X PATCH "http://localhost:8001/api/v1/cases/42" \
-H "Content-Type: application/json" \
-d '{"status": "lukket"}'
Response: 200 OK
{
"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 deleted400 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:
curl -X DELETE "http://localhost:8001/api/v1/cases/42"
Response: 200 OK
{
"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:
curl -X GET "http://localhost:8001/api/v1/cases/42/tags"
Response: 200 OK
[
{
"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:
{
"tag_navn": "high_priority",
"state": "open"
}
Note: state defaults to "open" if not provided.
Request Example:
curl -X POST "http://localhost:8001/api/v1/cases/42/tags" \
-H "Content-Type: application/json" \
-d '{"tag_navn": "high_priority"}'
Response: 201 Created
{
"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_navn500 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 IDtag_id(required): Tag ID
Request Body:
{
"state": "closed"
}
Valid states: "open" or "closed"
Request Example:
curl -X PATCH "http://localhost:8001/api/v1/cases/42/tags/12/state" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}'
Response: 200 OK
{
"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 value404 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 IDtag_id(required): Tag ID
Request Example:
curl -X DELETE "http://localhost:8001/api/v1/cases/42/tags/12"
Response: 200 OK
{
"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:
curl -X GET "http://localhost:8001/api/v1/cases/42/relations"
Response: 200 OK
[
{
"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:
{
"målsag_id": 55,
"relationstype": "blokkerer"
}
Valid relation types:
relateret- General relationafhænger af- Source depends on targetblokkerer- Source blocks targetduplikat- Source duplicates target
Request Example:
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
{
"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-reference404 Not Found- Target case not found500 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 IDrelation_id(required): Relation ID
Request Example:
curl -X DELETE "http://localhost:8001/api/v1/cases/42/relations/7"
Response: 200 OK
{
"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:
curl -X GET "http://localhost:8001/api/v1/cases/42/contacts"
Response: 200 OK
[
{
"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:
{
"contact_id": 15,
"role": "Primary Contact"
}
Note: role defaults to "Kontakt" if not provided.
Request Example:
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
{
"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 linked500 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 IDcontact_id(required): Contact ID
Request Example:
curl -X DELETE "http://localhost:8001/api/v1/cases/42/contacts/15"
Response: 200 OK
{
"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 case500 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:
curl -X GET "http://localhost:8001/api/v1/cases/42/customers"
Response: 200 OK
[
{
"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:
{
"customer_id": 456,
"role": "Main Customer"
}
Note: role defaults to "Kunde" if not provided.
Request Example:
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
{
"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 linked500 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 IDcustomer_id(required): Customer ID
Request Example:
curl -X DELETE "http://localhost:8001/api/v1/cases/42/customers/456"
Response: 200 OK
{
"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 case500 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:
curl -X GET "http://localhost:8001/api/v1/search/cases?q=network"
Response: 200 OK
[
{
"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:
curl -X GET "http://localhost:8001/api/v1/search/contacts?q=john"
Response: 200 OK
[
{
"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 short500 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:
curl -X GET "http://localhost:8001/api/v1/search/customers?q=acme"
Response: 200 OK
[
{
"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 short500 Internal Server Error- Search failed
Bulk Operations
Bulk Actions on Cases
Perform bulk actions on multiple cases simultaneously.
Endpoint: POST /cases/bulk
Request Body:
{
"case_ids": [42, 55, 67],
"action": "update_status",
"params": {
"status": "lukket"
}
}
Available Actions:
1. Update Status
{
"case_ids": [42, 55],
"action": "update_status",
"params": {
"status": "lukket"
}
}
2. Add Tag
{
"case_ids": [42, 55, 67],
"action": "add_tag",
"params": {
"tag_navn": "reviewed"
}
}
3. Close All
{
"case_ids": [42, 55, 67],
"action": "close_all",
"params": {}
}
Request Example:
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
{
"success": true,
"affected_cases": 3,
"action": "update_status"
}
Error Responses:
400 Bad Request- Missing fields or invalid action500 Internal Server Error- Bulk operation failed
Common Error Responses
All endpoints may return the following error codes:
400 Bad Request
{
"detail": "Missing required field: titel"
}
404 Not Found
{
"detail": "Case not found."
}
500 Internal Server Error
{
"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."