bmc_hub/docs/SAG_API.md
Christian 29acdf3e01 Add tests for new SAG module endpoints and module deactivation
- 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.
2026-01-31 23:16:24 +01:00

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 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:

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 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:

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_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:

{
  "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 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:

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 relation
  • afhænger af - Source depends on target
  • blokkerer - Source blocks target
  • duplikat - 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-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:

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 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:

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 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:

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 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:

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 case
  • 500 Internal Server Error - Failed to remove customer

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 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:

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 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:

{
  "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 action
  • 500 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."