- 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.
975 lines
19 KiB
Markdown
975 lines
19 KiB
Markdown
# 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."
|