REST API reference for the IT-service ticketing system. This document is generated from the actual Spring controllers and is the authoritative description of every endpoint the system exposes. For an always-live, interactive copy use the Swagger UI:
- Swagger UI:
http://localhost/swagger-ui/index.html - OpenAPI spec:
http://localhost/v3/api-docs
The system is made of two HTTP services:
| Service | Default port | Documented here |
|---|---|---|
it-service-backend — main API |
8081 |
Tickets, Comments, Attachments, Worklogs, CSAT, Users, Notifications, Notification Preferences, Products, Topics, Known Issues, Canned Responses, Agent-Product Limits, Dashboard Metrics, Admin Mail, Config, Internal/Workflow |
llm-service — AI summaries |
8082 |
AI Summaries |
All traffic normally enters through the nginx reverse proxy:
http://localhost/api/v1 # via nginx (recommended)
http://localhost:8081/api/v1 # it-service-backend, direct
http://localhost:8082/api/v1 # llm-service, direct
All backend endpoints are prefixed with /api/v1. Paths in this document are written relative
to the host (e.g. /api/v1/tickets).
The backend is a stateless OAuth2 resource server. Every non-public endpoint requires a
Keycloak-issued JWT access token in the Authorization header:
Authorization: Bearer <JWT>
Tokens are obtained from Keycloak directly (realm TicketSystemRealm); the frontend uses
keycloak-js. There is no username/password login endpoint on this API — /api/v1/auth/login
and /api/v1/auth/register are reserved/permit-listed paths handled by the Keycloak login flow,
not by a backend controller. Password reset is handled entirely by Keycloak's native
reset-credentials flow — the backend has no /api/v1/auth/** controller of its own (the former
custom forgot/reset-password endpoints were removed). Self-service change password (with the
current password) is still a backend endpoint: POST /api/v1/users/me/password.
Roles. The JWT's realm_access.roles are mapped to Spring authorities ROLE_<NAME>.
Authorization is additive multi-role: a user holds a set of roles and the effective
permission for an endpoint is the union — holding any one of the listed roles is enough.
The five application roles, by axis, are:
customer— end user: opens tickets and messages on own tickets (product-scoped).agent— operational fulfiller: claims tickets and acts only on claimed tickets (product-scoped).lead_agent— operational team lead; a Keycloak composite ofagent(so it inherits all agent abilities) plus: assign tickets to agents, act on tickets without claiming, manage product content (topics / known-issues / shared canned-responses) and a product-scoped team dashboard.admin— system configuration (global): create users, assign roles, create/manage products, grant product access, agent limits, SLA / cache.manager— oversight (global, read-only): all dashboards, reports and full read visibility; no operational actions and no system configuration.
A "super-admin" is simply a user that holds all of admin + lead_agent + manager (e.g. the
superadmin seed user) — there is no dedicated super-admin role. Roles are cached in the
user_roles table (Flyway V37), synced on /users/sync.
customer is a singleton role: an end user who opens tickets can never also be staff. Combining
customer with any other role is rejected with 400 (error.role.customer.exclusive) on role
assignment / user creation, keeping the customer and staff identity contexts mutually exclusive.
In this document the Role column lists who may call an endpoint:
Authenticated— any valid JWT (role-specific filtering happens in the service layer).- A specific role name (or list) — enforced by
@PreAuthorizeon the controller method; any one of the listed roles satisfies it. Internal token— not JWT; see below.
Internal endpoints. Paths under /api/v1/internal/** bypass JWT entirely. They require a
shared secret in the X-Internal-Token header (matching jbpm.kie-server.callback-token).
Used only for service-to-service calls (jBPM KIE Server, llm-service).
Public (anonymous) endpoints. /api/v1/auth/login, /api/v1/auth/register (reserved
permit-listed paths, no backend controller), /ws/** (WebSocket handshake), Swagger, and
/actuator/health|info|metrics.
The
llm-servicedoes not run Spring Security. Its/api/v1/ai/**endpoints are reached only over the internal Docker/K8s network and are not exposed through nginx to end users.
All handled errors return a consistent JSON body (ErrorResponse):
{
"status": 400,
"error": "TICKET_LIMIT_EXCEEDED",
"message": "Active ticket limit reached for this product.",
"fieldErrors": {
"title": "This field cannot be blank."
},
"timestamp": 1700000000000
}| Field | Type | Notes |
|---|---|---|
status |
int | HTTP status code. |
error |
string | Machine-readable error code. Present only for specific errors (USER_ALREADY_EXISTS, WRONG_CURRENT_PASSWORD, INVALID_PASSWORD, TICKET_LIMIT_EXCEEDED). Omitted otherwise. |
message |
string | Human-readable, localized to the caller's preferred language (en/tr). |
fieldErrors |
object | Field → message map. Present only for validation failures (400) and conflicts. |
timestamp |
long | Epoch milliseconds. |
Common status codes: 400 validation / business-rule violation, 401 missing or invalid
JWT, 403 authenticated but not permitted, 404 resource not found, 409 conflict
(duplicate user, ticket limit), 413 upload too large, 429 rate limit exceeded,
500 unexpected error.
List endpoints that support paging accept these query parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
page |
int | 0 |
Zero-based page index. |
size |
int | 20 |
Page size. Constrained to 1..500. |
sortBy |
string | createdAt |
Field to sort by (ticket lists). |
sortDir |
string | desc |
asc or desc (ticket lists). |
Most paginated endpoints return a Spring Page envelope:
{
"content": [ /* array of items */ ],
"pageable": { "pageNumber": 0, "pageSize": 20 },
"totalElements": 137,
"totalPages": 7,
"number": 0,
"size": 20,
"first": true,
"last": false,
"numberOfElements": 20,
"empty": false
}GET /api/v1/users returns a trimmed envelope instead:
{ "content": [...], "totalElements", "totalPages", "page", "size" }.
TicketController — base path /api/v1/tickets.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/tickets |
customer | Create a new ticket. |
| GET | /api/v1/tickets |
customer, agent, lead_agent, manager, admin | List tickets (role-scoped, paged, filtered). |
| GET | /api/v1/tickets/pool |
agent, lead_agent | Unclaimed NEW tickets in the agent's products. |
| GET | /api/v1/tickets/my-assigned |
Authenticated | Tickets the calling agent has claimed. |
| GET | /api/v1/tickets/team |
agent, lead_agent | Active tickets across the agent's authorized products. |
| GET | /api/v1/tickets/all |
agent, lead_agent, manager, admin | All tickets (all statuses) in authorized products. |
| GET | /api/v1/tickets/by-product/{productId} |
Authenticated | Tickets for one product (role-scoped). |
| GET | /api/v1/tickets/{id} |
Authenticated | Get one ticket with full detail. |
| GET | /api/v1/tickets/{id}/sla-timer |
customer, agent, lead_agent, manager, admin | Live SLA timer info for a ticket. |
| PUT | /api/v1/tickets/{id}/claim |
agent, lead_agent | Claim a ticket in any status except CLOSED. |
| DELETE | /api/v1/tickets/{id}/claim |
agent, lead_agent | Release the caller's own claim. |
| PUT | /api/v1/tickets/{id}/assign |
lead_agent, admin | Manually assign a ticket to a target agent. |
| PUT | /api/v1/tickets/{id}/wait |
agent, lead_agent | Action: put on hold (IN_PROGRESS → WAITING_FOR_CUSTOMER). |
| PUT | /api/v1/tickets/{id}/resume |
customer, agent, lead_agent | Action: resume after a hold (WAITING_FOR_CUSTOMER → IN_PROGRESS). |
| PUT | /api/v1/tickets/{id}/resolve |
agent, lead_agent | Action: resolve (IN_PROGRESS → RESOLVED; reason code required). |
| PUT | /api/v1/tickets/{id}/reopen |
customer, agent, lead_agent | Action: reopen (RESOLVED → IN_PROGRESS). |
| PUT | /api/v1/tickets/{id}/priority |
agent, lead_agent | Change ticket priority. |
| PUT | /api/v1/tickets/{id}/topic |
agent, lead_agent | Change ticket topic. |
| PUT | /api/v1/tickets/{id}/close |
customer, agent, lead_agent | Close a ticket (note + reason code). |
| DELETE | /api/v1/tickets/{id} |
admin | Permanently delete a ticket. |
All list endpoints (GET /api/v1/tickets, /pool, /my-assigned, /team, /all,
/by-product/{productId}) accept the pagination params plus these optional
filters (all repeatable list parameters):
| Param | Type | Description |
|---|---|---|
status |
string[] | Filter by status (NEW, IN_PROGRESS, WAITING_FOR_CUSTOMER, RESOLVED, CLOSED). Not accepted by /pool. |
priority |
string[] | Filter by priority (LOW, MEDIUM, HIGH, CRITICAL). |
search |
string | Case-insensitive free-text search over the ticket title (max 100 chars). |
productId |
long[] | Filter by product ID. Not accepted by /by-product/{productId}. |
agentId |
string[] | Filter by claiming agent's Keycloak ID. |
topicId |
long[] | Filter by topic ID. |
slaStatus |
string[] | Filter by SLA state. |
csatRating |
string[] | Filter by CSAT rating: 1–5 or NONE (no survey). Accepted only on /my-assigned and /all, and honoured only for admin/manager callers (silently ignored for others). |
dateFrom |
ISO date-time | Created-at lower bound. |
dateTo |
ISO date-time | Created-at upper bound. |
Sorting by csatRating (sortBy=csatRating) is likewise honoured only for admin/manager on
/my-assigned and /all; other callers fall back to createdAt.
Body TicketRequestDTO:
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | Not blank, max 100 chars. |
description |
string | yes | Not blank, max 500 chars. Also stored as the first comment. |
priority |
string | yes | LOW, MEDIUM, HIGH, CRITICAL. |
productId |
long | yes | Product/category the ticket belongs to. |
topicId |
long | conditional | Topic ID; must be an active topic of productId. May be omitted/null only when the product has no active topics (a topicless ticket); otherwise required. |
Request:
POST /api/v1/tickets
Authorization: Bearer <JWT>
{
"title": "Cannot connect to VPN",
"description": "VPN times out since this morning. Error: ERR_TIMEOUT",
"priority": "HIGH",
"productId": 1,
"topicId": 12
}Response 200 OK (TicketResponseDTO):
{
"id": 42,
"title": "Cannot connect to VPN",
"description": "VPN times out since this morning. Error: ERR_TIMEOUT",
"status": "NEW",
"priority": "HIGH",
"productId": 1,
"productNameTr": "Müşteri Yönetimi",
"productNameEn": "CRM",
"topicId": 12,
"topicNameTr": "Şifre sıfırlama",
"topicNameEn": "Password reset",
"customerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customerName": "Ali Yilmaz",
"claimers": [],
"slaDeadline": "2026-05-22T17:00:00+03:00",
"slaBreached": false,
"slaElapsedMs": 0,
"slaPausedAt": null,
"createdAt": "2026-05-21T09:30:00+03:00",
"resolvedAt": null,
"closedAt": null,
"hasCsat": false,
"csatRating": null,
"slaInfo": { "slaState": "active", "remainingMs": 27000000 },
"auditLogs": []
}GET /api/v1/tickets?page=0&size=20&status=NEW&status=IN_PROGRESS&priority=HIGH&sortBy=createdAt&sortDir=descResponse is a Page envelope whose content is an array of TicketResponseDTO.
Returns a single TicketResponseDTO (same shape as the create response, including
claimers, slaInfo and auditLogs). Path param id (long).
The csatRating field (1–5) is populated only for admin/manager callers; all other roles
receive null. Likewise the CSAT_SUBMITTED audit-log entry (which carries the rating/comment)
is included only for admin/manager and the ticket's own customer — agents/leads do not see it.
auditLogs[] items (TicketAuditLogDTO):
{
"id": 7,
"actorId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"actorName": "Mehmet Demir",
"actionType": "CLAIM",
"reasonCode": null,
"note": null,
"previousState": "NEW",
"newState": "IN_PROGRESS",
"createdAt": "2026-05-21T10:05:00+03:00"
}PUT /api/v1/tickets/{id}/claim — no body. Allowed in any status except CLOSED; claiming a CLOSED ticket returns 400. The first claim auto-promotes a NEW ticket to IN_PROGRESS, advancing the BPMN state machine via a transition signal.
DELETE /api/v1/tickets/{id}/claim — Body UnclaimRequestDTO:
reasonCode (string, required), note (string, required when reasonCode is OTHER).
Releasing the last claim moves the ticket IN_PROGRESS → NEW, also driven through the BPMN.
PUT /api/v1/tickets/{id}/assign — Body AssignTicketRequestDTO:
targetAgentId (string, required — Keycloak ID), note (string, optional). Capacity of the
target agent is checked; returns 400/409 if the agent's limit is full. Like claim, assigning
a NEW ticket advances it to IN_PROGRESS through the BPMN state machine.
Status action endpoints — PUT /api/v1/tickets/{id}/wait, /resume, /resolve, /reopen.
There is no generic status-change endpoint anymore (the former PUT /api/v1/tickets/{id}/status
was removed). The caller runs an action, not a free-form target status — each action drives the
status within its own guard: it is idempotent if already at the target, and returns 400 if the
source state is incompatible. The NEW ↔ IN_PROGRESS transition is driven by claim/unclaim only,
keeping the claim/status invariant intact. Each action takes an optional TicketActionRequestDTO
body (resolve requires it because the reason code is mandatory):
| Field | Type | Required | Notes |
|---|---|---|---|
reasonCode |
string | conditional | Required for resolve; optional for wait/resume/reopen. |
note |
string | conditional | Required when reasonCode is OTHER. |
| Action | Transition | Roles |
|---|---|---|
PUT .../wait |
IN_PROGRESS → WAITING_FOR_CUSTOMER |
agent, lead_agent |
PUT .../resume |
WAITING_FOR_CUSTOMER → IN_PROGRESS |
customer, agent, lead_agent |
PUT .../resolve |
IN_PROGRESS → RESOLVED |
agent, lead_agent |
PUT .../reopen |
RESOLVED → IN_PROGRESS |
customer, agent, lead_agent |
The BPMN process is the authoritative state machine: a transition the BPMN does not accept is
rejected with 400.
PUT /api/v1/tickets/42/resolve
{ "reasonCode": "SOLUTION_PROVIDED", "note": "Fix sent by email." }PUT /api/v1/tickets/{id}/priority — Body PriorityChangeRequestDTO:
priority (string, required), reasonCode (string, required), note (string — required when
reasonCode is OTHER).
PUT /api/v1/tickets/{id}/topic — Body TopicChangeRequestDTO:
topicId (long, required — an active topic of the same product), reasonCode (string,
required), note (string — required when reasonCode is OTHER).
PUT /api/v1/tickets/{id}/close — customer, agent, lead_agent. Body
CloseTicketRequestDTO: reasonCode (string, required), note (string — required when
reasonCode is OTHER).
DELETE /api/v1/tickets/{id} — admin only; no body; returns 204 No Content.
GET /api/v1/tickets/{id}/sla-timer — customer, agent, lead_agent, manager, admin.
Returns a JSON object describing the live SLA timer,
e.g. { "slaState": "active", "remainingMs": 27000000, "deadlineTimestamp": 1780346400000 }
(deadlineTimestamp is epoch-ms, and is -1 while paused/closed). slaState is
one of active, paused, expired, completed. The SLA counts only active time: while
paused (WAITING_FOR_CUSTOMER/RESOLVED) remainingMs is frozen (SLA budget minus accumulated
active elapsed) and does not decrease; on resume (back to IN_PROGRESS) the countdown continues
from that frozen value — time spent paused is not lost.
CommentController — base path /api/v1/tickets/{ticketId}/comments. All endpoints require an
authenticated user; role-based filtering is applied in the service layer.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/tickets/{ticketId}/comments |
Authenticated | Add a comment to a ticket. |
| GET | /api/v1/tickets/{ticketId}/comments |
Authenticated | List a ticket's comments (filtered by role). |
Comment types: EXTERNAL (visible to the customer), INTERNAL (operational staff — agent /
lead_agent — only, hidden from customers). Customers may only add EXTERNAL comments to their
own tickets. Posting is restricted to operational roles plus the owning customer: only an
agent/lead_agent (or the ticket's own customer) may add a comment. A pure admin or
manager is rejected server-side (error.comment.role.forbidden) — to comment they must also hold
lead_agent, aligning comments with the worklog/attachment model. (Reading still follows the
role-based visibility filter.)
A per-user posting cooldown (default 3s, COMMENT_COOLDOWN_SECONDS) and the max message
length (COMMENT_MAX_LENGTH, default 500) are enforced server-side; clients read the current
values from GET /api/v1/config/comments (see Config) rather than hardcoding them.
Path param ticketId (long). Body CommentRequestDTO:
| Field | Type | Required | Notes |
|---|---|---|---|
message |
string | yes | Not blank, max 500 chars. |
type |
string | yes | EXTERNAL or INTERNAL. |
Request:
POST /api/v1/tickets/42/comments
{ "message": "I checked your VPN config, try again.", "type": "EXTERNAL" }Response 200 OK (CommentDTO):
{
"id": 128,
"authorId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"authorName": "Mehmet Demir",
"authorRole": "AGENT",
"message": "I checked your VPN config, try again.",
"type": "EXTERNAL",
"createdAt": "2026-05-21T11:30:00+03:00"
}Returns a JSON array of CommentDTO ordered chronologically. Customers receive only
EXTERNAL comments; operational staff (agent / lead_agent) receive both types.
ConfigController — base path /api/v1/config. Read-only client configuration so the frontend
mirrors backend settings instead of hardcoding them.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/config/comments |
Authenticated | Comment posting cooldown (seconds) and max length. |
Response 200 OK:
{ "cooldownSeconds": 3, "maxLength": 500 }These mirror the COMMENT_COOLDOWN_SECONDS and COMMENT_MAX_LENGTH (default 500) settings.
AdminMailController — base path /api/v1/admin/mail. Admin-only SMTP diagnostics, so the
configured mail server can be validated end-to-end without triggering a full ticket flow.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/admin/mail/test |
admin | Send a test email to the calling admin's own address (synchronously) and report the outcome. |
POST /api/v1/admin/mail/test — no body. The mail is sent only to the calling admin's own
address (not an arbitrary recipient), so the endpoint cannot be abused as an open relay. Returns
200 OK:
{ "success": true, "recipient": "admin@example.com", "error": "" }On failure success is false and error carries the reason.
AttachmentController — base path /api/v1. File content is stored in the database as BYTEA.
Max file size 10 MB; text-based files are scanned for secret-like patterns.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/tickets/{ticketId}/attachments |
customer, agent, lead_agent | Upload a file to a ticket. |
| GET | /api/v1/tickets/{ticketId}/attachments |
customer, agent, lead_agent, manager, admin | List a ticket's attachment metadata. |
| GET | /api/v1/attachments/{id} |
customer, agent, lead_agent, manager, admin | Download a file's content. |
| DELETE | /api/v1/attachments/{id} |
customer, agent, lead_agent | Delete a file. |
POST /api/v1/tickets/{ticketId}/attachments — multipart/form-data with a single part
file. Path param ticketId (long). Returns 200 OK with AttachmentDTO metadata;
returns 413 when the file exceeds the size limit.
{
"id": 55,
"fileName": "error_screenshot.png",
"fileType": "image/png",
"uploaderId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"createdAt": "2026-05-21T12:00:00+03:00"
}GET /api/v1/tickets/{ticketId}/attachments — returns a JSON array of AttachmentDTO.
GET /api/v1/attachments/{id} — returns the raw bytes with the original Content-Type and a
Content-Disposition: attachment; filename="..." header. Path param id (long).
DELETE /api/v1/attachments/{id} — returns 204 No Content. Customers may delete only
their own uploads; agents only on their claimed tickets; lead_agent any (within authorized products).
TicketWorklogController — base path /api/v1/tickets. Tracks agent time spent on tickets.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/tickets/{id}/worklogs |
agent, lead_agent | Add a worklog entry. |
| GET | /api/v1/tickets/{id}/worklogs |
agent, lead_agent, manager, admin | List a ticket's worklogs (manager/admin read-only). |
| PUT | /api/v1/tickets/{id}/worklogs/{worklogId} |
agent, lead_agent | Update a worklog entry. |
| DELETE | /api/v1/tickets/{id}/worklogs/{worklogId} |
agent, lead_agent | Delete a worklog entry. |
| GET | /api/v1/tickets/all-worklogs |
manager, admin | List every worklog in the system. |
POST/PUT body WorklogRequestDTO: minutes (int, required, ≥ 1), description
(string, optional, max 500 chars).
Request:
POST /api/v1/tickets/42/worklogs
{ "minutes": 45, "description": "Reviewed firewall logs, updated port rules." }Response 201 Created (WorklogResponseDTO):
{
"id": 15,
"ticketId": 42,
"agentId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"agentName": "Mehmet Demir",
"minutes": 45,
"description": "Reviewed firewall logs, updated port rules.",
"createdAt": "2026-05-21T14:00:00+03:00",
"updatedAt": "2026-05-21T14:00:00+03:00"
}GET endpoints return JSON arrays of WorklogResponseDTO; each entry carries agentName
(the agent's display name) alongside agentId. Agents may update/delete only
their own worklogs; lead_agent may delete any. DELETE returns 204 No Content.
TicketCsatController — base path /api/v1/tickets. Surveys filled in at ticket closing.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/tickets/{id}/csat |
customer | Submit a CSAT survey for a resolved ticket. |
| GET | /api/v1/tickets/{id}/csat |
agent, lead_agent, manager, admin | Get the CSAT result of a ticket. |
| GET | /api/v1/tickets/all-csats |
manager, admin | List every CSAT result. |
POST /api/v1/tickets/{id}/csat — Body CsatDTO: rating (int, required, 1–5),
comment (string, optional). The ticket must be in RESOLVED status and owned by the caller;
submitting the survey transitions the ticket to CLOSED. One survey per ticket.
Request:
POST /api/v1/tickets/42/csat
{ "rating": 5, "comment": "Resolved quickly, thanks!" }Response 200 OK (Csat entity):
{
"id": 9,
"ticketId": 42,
"rating": 5,
"comment": "Resolved quickly, thanks!",
"createdAt": "2026-05-21T16:00:00+03:00"
}UserController — base path /api/v1/users.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/users/sync |
Authenticated | Sync the logged-in user from the JWT into the local DB (also refreshes the cached user_roles set). |
| GET | /api/v1/users |
admin, manager | List users (paged, searchable, role-filterable). |
| GET | /api/v1/users/{id} |
self, admin, manager | Get a user by Keycloak ID (callers may always read their own record). |
| GET | /api/v1/users/agents |
agent, lead_agent, admin, manager | List all agent users with their authorized products. |
| GET | /api/v1/users/agents/capacity |
lead_agent, admin | List agents with current load/limit for a product. |
| PUT | /api/v1/users/me |
Authenticated | Update the caller's profile (name, email). |
| POST | /api/v1/users/me/password |
Authenticated | Change the caller's password. |
| PUT | /api/v1/users/me/language |
Authenticated | Update the caller's preferred language. |
| PUT | /api/v1/users/me/theme |
Authenticated | Update the caller's preferred theme. |
| PUT | /api/v1/users/me/date-format |
Authenticated | Update the caller's preferred date display format. |
| GET | /api/v1/users/me/2fa |
Authenticated | List the caller's registered TOTP devices. |
| DELETE | /api/v1/users/me/2fa/{credentialId} |
Authenticated | Delete one of the caller's TOTP devices. |
| POST | /api/v1/users/me/2fa/notify-added |
Authenticated | Trigger the "2FA device added" notification email. |
| GET | /api/v1/users/me/pdf-preferences |
Authenticated | Get the caller's last-used PDF export modal selections. |
| PUT | /api/v1/users/me/pdf-preferences |
Authenticated | Persist the caller's PDF export modal selections. |
| PUT | /api/v1/users/me/panel-preferences |
Authenticated | Persist the caller's sidebar ticket-panel visibility selections. |
| PUT | /api/v1/users/me/onboarding-complete |
Authenticated | Mark the caller's onboarding as completed (idempotent). |
| POST | /api/v1/users/{userId}/products/{productId} |
admin | Grant an agent access to a product. |
| DELETE | /api/v1/users/{userId}/products/{productId} |
admin | Revoke an agent's product access. |
| PUT | /api/v1/users/{userId}/status |
admin | Activate / deactivate a user. |
| PUT | /api/v1/users/{userId}/roles |
admin | Replace a user's realm roles. |
| POST | /api/v1/users/admin/create |
admin | Create a new Keycloak user. |
| GET | /api/v1/users/admin/roles |
admin | List assignable realm roles. |
No body — the user is derived from the JWT. Returns a UserDTO:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "user@example.com",
"fullName": "Ali Yilmaz",
"role": "AGENT",
"isActive": true,
"preferredLanguage": "tr",
"preferredTheme": "dark",
"preferredDateFormat": "DMY_SLASH",
"createdAt": "2026-01-15T09:00:00+03:00",
"authorizedProducts": [
{ "id": 1, "name": "CRM", "isActive": true, "maxActiveTickets": 5 }
]
}The
rolefield carries the user's primary role for display/routing. Because roles are additive, the authoritative set of all roles the user holds is cached server-side in theuser_rolestable (Flyway V37, synced on this call) and drives@PreAuthorizechecks — a user may, for example, hold bothagentandmanager.
Query params: search (string, optional), role (string[], optional),
excludeGlobalRoles (boolean, default false), productId (long[], optional),
sortBy (string, default name), sortDir (string, default asc), page (int, ≥ 0),
size (int, 1–500). Returns the trimmed envelope:
{ "content": [UserDTO...], "totalElements", "totalPages", "page", "size" }.
- GET
/api/v1/users/agents/capacity— queryproductId(long, required). ReturnsAgentCapacityDTO[]. - PUT
/api/v1/users/me— BodyUpdateProfileRequest:firstName,lastName(≤ 50 chars each),email(valid email). All required. ReturnsUserDTO.409if email is taken. - POST
/api/v1/users/me/password— BodyChangePasswordRequest:currentPassword(required),newPassword(required, ≥ 8 chars, must satisfy realm policy). Returns204 No Content;400if the current password is wrong or the new one violates policy. - PUT
/api/v1/users/me/language— querylang(string:enortr). ReturnsUserDTO. - PUT
/api/v1/users/me/theme— querytheme(string:lightordark). ReturnsUserDTO. - PUT
/api/v1/users/me/date-format— queryformat(string: one ofDMY_SLASH,MDY_SLASH,YMD_DASH,DMY_DOT,MED). Drives every date shown in the UI. ReturnsUserDTO. - GET
/api/v1/users/me/pdf-preferences— returnsPdfPreferencesDTOcontaining the caller's last-used PDF export modal selections (selected sections, language, orientation, etc.) as an opaque JSON string. Returnsnullif no preference has been saved yet. - PUT
/api/v1/users/me/pdf-preferences— BodyPdfPreferencesDTO; persists the export modal selections verbatim. The frontend reads this back to pre-fill the modal on next open. Returns204 No Content. - PUT
/api/v1/users/me/panel-preferences— BodyPanelPreferencesDTO; persists the agent/lead sidebar ticket-panel visibility selections (workspace, pool, history, team, all-tickets) verbatim as an opaque JSON string (max 500 chars). Hydrated back to the client via/users/sync. Returns204 No Content. - PUT
/api/v1/users/me/onboarding-complete— no body. Marks the caller's onboarding flow as completed. Idempotent. Returns204 No Content. - PUT
/api/v1/users/{userId}/status— queryactive(boolean). An admin cannot deactivate themselves (400). ReturnsUserDTO. - PUT
/api/v1/users/{userId}/roles— Body: JSON array of role strings (non-empty); roles are additive, so a user may hold several, e.g.["agent","lead_agent"]or["manager","admin"]. ReturnsUserDTO. Assignable roles arecustomer,agent,lead_agent,admin,manager.customeris a singleton — combining it with any other role is rejected with400. - POST
/api/v1/users/admin/create— BodyCreateUserRequest(see below). Returns201 CreatedwithUserCreationResponseDTO.409if email/username already exists.
CreateUserRequest:
| Field | Type | Required | Notes |
|---|---|---|---|
username |
string | yes | Unique, 3–50 chars. |
email |
string | yes | Unique, valid email. |
firstName |
string | yes | ≤ 50 chars. |
lastName |
string | yes | ≤ 50 chars. |
password |
string | yes | ≥ 8 chars (temporary password by default). |
roles |
string[] | yes | At least one realm role. |
temporaryPassword |
boolean | no | Default true. |
POST /api/v1/users/admin/create
{
"username": "john.doe",
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"password": "Temp1234!",
"roles": ["agent"],
"temporaryPassword": true
}Response 201 Created:
{
"keycloakId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"username": "john.doe",
"email": "john.doe@example.com",
"fullName": "John Doe",
"assignedRoles": ["agent"]
}NotificationController — base path /api/v1/notifications. All endpoints require an
authenticated user and operate only on the caller's own notifications.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/notifications |
Authenticated | List the caller's notifications (paged). |
| GET | /api/v1/notifications/unread-count |
Authenticated | Count of unread notifications. |
| PATCH | /api/v1/notifications/{id}/read |
Authenticated | Mark one notification as read. |
| POST | /api/v1/notifications/read-all |
Authenticated | Mark all notifications as read. |
| DELETE | /api/v1/notifications/{id} |
Authenticated | Delete one notification. |
| DELETE | /api/v1/notifications |
Authenticated | Delete all of the caller's notifications. |
GET /api/v1/notifications — query page (int, ≥ 0), size (int, 1–500). Returns a
Page envelope whose content is an array of NotificationResponse:
{
"id": 42,
"userId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"message": "Ticket #12 has been assigned.",
"isRead": false,
"createdAt": "2026-05-21T13:00:00+03:00",
"type": "TICKET_ASSIGNED",
"referenceId": 12,
"referenceType": "TICKET"
}GET /api/v1/notifications/unread-count — returns {"count": 3}.
PATCH, POST /read-all, and the DELETE endpoints return 204 No Content.
NotificationPreferenceController — base path /api/v1/notification-preferences. Each user
reads and writes their own preferences.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/notification-preferences |
Authenticated | Get the caller's notification preferences. |
| PUT | /api/v1/notification-preferences |
Authenticated | Update the caller's notification preferences. |
Both return NotificationPreferenceResponse. If no preference row exists, all flags default
to true. On PUT, fields sent as null keep their current value (UpdateNotificationPreferenceRequest).
{
"emailOnTicketCreated": true,
"emailOnTicketAssigned": true,
"emailOnStatusChanged": true,
"emailOnCommentAdded": true,
"emailOnSlaWarning": true,
"emailOnSlaBreached": true,
"emailOnTicketResolved": true,
"notifyOnTicketCreated": true,
"notifyOnTicketAssigned": true,
"notifyOnStatusChanged": true,
"notifyOnCommentAdded": true,
"notifyOnSlaWarning": true,
"notifyOnSlaBreached": true,
"notifyOnTicketResolved": true
}ProductController — base path /api/v1/products. Products are the support categories.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/products |
Authenticated | List products visible to the caller's role. |
| GET | /api/v1/products/{id} |
Authenticated | Get a single product. |
| POST | /api/v1/products |
admin | Create a product. |
| PUT | /api/v1/products/{id} |
admin | Update a product's name / active flag. |
| PATCH | /api/v1/products/{id}/limit |
admin | Update the product's default concurrent-ticket limit. |
| DELETE | /api/v1/products/{id} |
admin | Delete a product. |
GET /api/v1/products — customer / agent see only their authorized products;
admin and manager see all. Product/category names are bilingual: both nameTr and
nameEn variants are returned (at least one is non-null) and the client picks the variant matching
its UI language, falling back to the other. Returns a JSON array of ProductDTO:
[
{ "id": 1, "nameTr": "Müşteri Yönetimi", "nameEn": "CRM", "isActive": true, "maxActiveTickets": 5 },
{ "id": 2, "nameTr": null, "nameEn": "ERP", "isActive": true, "maxActiveTickets": null }
]POST / PUT /api/v1/products — Body is a Product entity with the bilingual name fields, e.g.
{ "nameTr": "Kurumsal Kaynak", "nameEn": "ERP", "isActive": true } (at least one of nameTr /
nameEn required). Returns ProductDTO.
PATCH /api/v1/products/{id}/limit — Body ProductLimitUpdateRequestDTO:
maxActiveTickets (int, nullable — null removes the limit). Returns ProductDTO.
DELETE /api/v1/products/{id} — returns 204 No Content.
TicketTopicController — topics are sub-categories belonging to a product. (Controller has no
class-level base path; full paths are shown below.)
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/products/{productId}/topics |
Authenticated | List a product's topics. |
| POST | /api/v1/products/{productId}/topics |
lead_agent | Create a topic under a product. |
| PUT | /api/v1/topics/{id} |
lead_agent | Update a topic's name / active flag. |
| DELETE | /api/v1/topics/{id} |
lead_agent | Delete a topic. |
GET /api/v1/products/{productId}/topics — query includeInactive (boolean, default
false). Topic names are bilingual (nameTr / nameEn, client picks by UI language with
fallback). Returns a JSON array of TicketTopicDTO:
[
{ "id": 12, "productId": 3, "nameTr": "Şifre sıfırlama", "nameEn": "Password reset", "isActive": true }
]POST / PUT body TicketTopicDTO: nameTr / nameEn (each ≤ 255 chars; at least one
non-blank required on create — isActive-only updates may omit both), isActive (boolean).
Returns TicketTopicDTO. DELETE returns 204 No Content.
KnownIssueController — knowledge-base entries tied to a product (and optionally a topic).
(No class-level base path; full paths shown below.) List/detail require product authorization;
write operations require lead_agent (product content management).
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/products/{productId}/known-issues |
Authenticated | List a product's known issues. |
| GET | /api/v1/known-issues/{id} |
Authenticated | Get one known-issue entry. |
| POST | /api/v1/products/{productId}/known-issues |
lead_agent | Create a known-issue entry. |
| PUT | /api/v1/known-issues/{id} |
lead_agent | Update a known-issue entry. |
| DELETE | /api/v1/known-issues/{id} |
lead_agent | Delete a known-issue entry. |
GET /api/v1/products/{productId}/known-issues — query topicId (long, optional),
includeInactive (boolean, default false). Returns a JSON array of KnownIssueDTO.
Title and content are bilingual (*Tr / *En variants); the client picks the variant
matching its UI language with fallback.
POST / PUT body KnownIssueDTO:
| Field | Type | Required | Notes |
|---|---|---|---|
topicId |
long | no | Optional topic association. |
titleTr |
string | conditional | Turkish title (≤ 255 chars). At least one of titleTr / titleEn required. |
titleEn |
string | conditional | English title (≤ 255 chars). |
contentTr |
string | conditional | Turkish content (≤ 10000 chars). At least one of contentTr / contentEn required. |
contentEn |
string | conditional | English content (≤ 10000 chars). |
isActive |
boolean | no | Whether shown to users. |
Response KnownIssueDTO:
{
"id": 42,
"productId": 3,
"topicId": 12,
"titleTr": "VPN bağlantısı kopuyor",
"titleEn": "VPN connection drops",
"contentTr": "Ağ ayarlarınızı kontrol edip ...",
"contentEn": "Check your network settings and ...",
"isActive": true,
"createdBy": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"createdAt": "2026-05-10T09:00:00+03:00",
"updatedAt": "2026-05-12T11:00:00+03:00"
}DELETE returns 204 No Content.
CannedResponseController — base path /api/v1/canned-responses. Reusable reply templates
for agents. Every endpoint requires one of agent, lead_agent, or admin — customers cannot
access this feature.
Two scopes: PERSONAL (owned by the creating agent, only they can see/edit/delete it) and
SHARED (product-wide or global; only lead_agent / admin may create or mutate shared
templates). Three visibility values control which comment type the template targets: EXTERNAL,
INTERNAL, or BOTH.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/canned-responses |
agent, lead_agent, admin | List canned responses visible to the caller (composer picker; non-paged). |
| GET | /api/v1/canned-responses/paged |
agent, lead_agent, admin | Paged + filtered list for the management screen. |
| POST | /api/v1/canned-responses |
agent, lead_agent, admin | Create a canned response. |
| PUT | /api/v1/canned-responses/{id} |
agent, lead_agent, admin | Update a canned response. |
| DELETE | /api/v1/canned-responses/{id} |
agent, lead_agent, admin | Delete a canned response. |
| POST | /api/v1/canned-responses/{id}/favorite |
agent, lead_agent, admin | Mark a response as a favorite (idempotent). |
| DELETE | /api/v1/canned-responses/{id}/favorite |
agent, lead_agent, admin | Remove a favorite mark (idempotent). |
Returns the caller's own personal templates plus all shared templates (global shared + those tied to the caller's authorized products). Optional query params:
| Param | Type | Description |
|---|---|---|
productId |
long | Scope shared templates to this product (global + product-specific). |
scope |
string | PERSONAL or SHARED. |
visibility |
string | EXTERNAL, INTERNAL, or BOTH. |
q |
string | Free-text search over title and shortcut. |
Returns a JSON array of CannedResponseDTO.
Server-side filtered + paginated variant for the management list. Accepts productId (long,
specific product) or global (boolean, productless templates only), scope, visibility,
lang (tr/en), q (search), plus page/size. Results are ordered favorites-first then by
updatedAt descending. Returns a Page envelope of CannedResponseDTO.
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | Max 150 chars. Language-neutral management label. |
shortcut |
string | no | Max 50 chars. Slash-command shortcut (without the /). |
contentTr |
string | conditional | Turkish content variant (max 2000 chars). At least one of contentTr / contentEn required. |
contentEn |
string | conditional | English content variant (max 2000 chars). |
scope |
string | yes | PERSONAL or SHARED. Creating SHARED requires lead_agent or admin. |
productId |
long | no | Optional product association (meaningful only for SHARED; null = global). |
visibility |
string | yes | EXTERNAL, INTERNAL, or BOTH. |
Response CannedResponseDTO:
{
"id": 7,
"title": "VPN bağlantı adımları",
"shortcut": "vpn",
"contentTr": "VPN sorununuzu çözmek için şu adımları uygulayın: ...",
"contentEn": "To resolve your VPN issue, follow these steps: ...",
"scope": "PERSONAL",
"ownerAgentId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"productId": null,
"visibility": "EXTERNAL",
"favorite": false,
"createdAt": "2026-05-10T09:00:00+03:00",
"updatedAt": "2026-05-10T09:00:00+03:00"
}DELETE returns 204 No Content. POST /{id}/favorite and DELETE /{id}/favorite return
204 No Content (both idempotent).
AgentProductLimitController — base path /api/v1/agents/{agentId}/limits. Per-agent overrides
of the product-level concurrent-ticket limit.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/agents/{agentId}/limits |
admin | List all product-limit overrides for an agent. |
| PUT | /api/v1/agents/{agentId}/limits/{productId} |
admin | Create / update an agent's limit for a product. |
| DELETE | /api/v1/agents/{agentId}/limits/{productId} |
admin | Remove an agent/product override. |
PUT body AgentProductLimitRequestDTO: useCustomLimit (boolean),
maxActiveTickets (int, nullable).
Response AgentProductLimitResponseDTO:
{
"agentId": "f9e8d7c6-b5a4-3210-fedc-ba0987654321",
"productId": 10,
"productName": "CRM",
"useCustomLimit": true,
"maxActiveTickets": 3,
"effectiveLimit": 3
}GET returns a JSON array of the above; DELETE returns 204 No Content.
MetricsController — base path /api/v1/metrics. Aggregated KPIs and analytics. The management
dashboards are open to manager, lead_agent and admin; results are Caffeine-cached
(5-minute TTL). Scope is resolved from the JWT: admin/manager see everything (global), while a
pure lead_agent is restricted to the products they are authorized on (agent-performance
returns that product-scoped team dashboard).
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/metrics/dashboard-summary |
manager, lead_agent, admin | Headline KPIs (open tickets, SLA breach rate, response time, CSAT). |
| GET | /api/v1/metrics/status-distribution |
manager, lead_agent, admin | Ticket counts per status. |
| GET | /api/v1/metrics/agent-performance |
manager, lead_agent, admin | Agent leaderboard (load, resolution speed, CSAT, SLA). lead_agent sees its product-scoped team dashboard; admin/manager see all. |
| GET | /api/v1/metrics/ticket-timeline |
manager, lead_agent, admin | Daily created/resolved/closed/breach trend. |
| GET | /api/v1/metrics/priority-sla-metrics |
manager, lead_agent, admin | SLA metrics broken down by priority. |
| GET | /api/v1/metrics/product-metrics |
manager, lead_agent, admin | Per-product ticket metrics. |
| GET | /api/v1/metrics/csat-metrics |
manager, lead_agent, admin | Detailed CSAT analytics. |
| GET | /api/v1/metrics/alerts-backlog |
manager, lead_agent, admin | SLA-breach alerts and backlog summary. |
| GET | /api/v1/metrics/worklog-completion |
manager, lead_agent, admin | Worklog totals and ticket-completion stats. |
| Method | Endpoint | Role | Description |
|---|---|---|---|
| GET | /api/v1/metrics/me/customer |
Authenticated | The caller's own customer dashboard (tickets they opened). Non-customers simply see zeros. |
| GET | /api/v1/metrics/me/agent |
agent, lead_agent | The caller's own agent-performance dashboard (tickets they claimed + their worklogs). |
| GET | /api/v1/metrics/users/{userId}/agent |
manager, lead_agent, admin | A specific user's agent dashboard. admin/manager global; a pure lead_agent sees only the slice within its authorized products and gets 403 for agents sharing no product. |
| GET | /api/v1/metrics/users/{userId}/customer |
manager, admin | A specific user's customer dashboard. Leads have no access. |
| GET | /api/v1/metrics/products/{productId}/dashboard |
manager, lead_agent, admin | Dedicated single-product dashboard. admin/manager any product; a pure lead_agent only products it is authorized on (else 403). |
Query parameters:
| Endpoint | Param | Type | Default |
|---|---|---|---|
/ticket-timeline |
days |
int | 30 |
/priority-sla-metrics |
days |
int (optional) | — |
/product-metrics |
days |
int (optional) | — |
/csat-metrics |
months |
int | 3 |
/worklog-completion |
days |
int | 30 |
/me/customer, /me/agent, /users/{userId}/agent, /users/{userId}/customer, /products/{productId}/dashboard |
days |
int (optional) | 30 (clamped 1–365) |
Each endpoint returns its dedicated DTO (DashboardMetricsDTO, StatusDistributionDTO,
AgentPerformanceDTO, TicketTimelineDTO, PrioritySLAMetricsDTO, ProductMetricsDTO,
CSATMetricsDTO, AlertsBacklogDTO, WorklogCompletionDTO, CustomerDashboardDTO,
AgentDashboardDTO, ProductDashboardDTO). Example
GET /api/v1/metrics/dashboard-summary:
{
"openTickets": 137,
"slaBreachRate": 4.2,
"averageResponseTimeHours": 3.6,
"averageCsat": 4.4,
"priorityDistribution": { "LOW": 40, "MEDIUM": 60, "HIGH": 30, "CRITICAL": 7 }
}Field names of metric DTOs are illustrative — consult the Swagger UI / OpenAPI spec for the exact schema of each metrics response.
AiSummaryController — served by llm-service at base path /api/v1/ai/summaries
(port 8082). These endpoints have no Spring Security; they are called service-to-service
(by it-service-backend / internal callers) and are not exposed to end users via nginx.
| Method | Endpoint | Role | Description |
|---|---|---|---|
| POST | /api/v1/ai/summaries |
Internal | Summarize a ticket from a supplied raw payload. |
| POST | /api/v1/ai/summaries/tickets/{ticketId}/generate |
Internal | Fetch ticket data and generate a summary. |
| GET | /api/v1/ai/summaries/tickets/{ticketId}/latest |
Internal | Get the most recent summary for a ticket. |
| GET | /api/v1/ai/summaries/tickets/{ticketId} |
Internal | List all summaries for a ticket (newest first). |
POST /api/v1/ai/summaries — Body SummarizeRequestDTO: ticketId (long), ticket
(object), comments (array), worklogs (array), resolutionNote (object, optional),
knownIssues (array), language (string, tr or en, default tr).
POST /api/v1/ai/summaries/tickets/{ticketId}/generate — path param ticketId (long),
query language (string, default tr). llm-service pulls the ticket data from
it-service-backend (GET /api/v1/internal/tickets/{ticketId}/full), sends it to the Groq LLM,
and persists the result.
Response AiSummaryResponseDTO:
{
"id": 7,
"ticketId": 42,
"model": "llama-3.1-8b-instant",
"promptTokens": 850,
"completionTokens": 120,
"summary": "The customer reported a VPN timeout. The agent identified a blocked port and ...",
"createdAt": "2026-05-21T15:30:00+03:00"
}GET .../tickets/{ticketId} returns a JSON array of AiSummaryResponseDTO.
These endpoints are authenticated by the X-Internal-Token header (not JWT). They live under
/api/v1/internal/** and are used only for service-to-service communication.
InternalTicketController — base path /api/v1/internal/tickets.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/v1/internal/tickets/{ticketId}/full |
X-Internal-Token |
Full ticket bundle (ticket, comments, worklogs, known issues) — consumed by llm-service. |
Returns a JSON object: { "ticket": TicketResponseDTO, "comments": [CommentDTO], "worklogs": [WorklogResponseDTO], "knownIssues": [KnownIssueDTO] }.
WorkflowCallbackController — base path /api/v1/internal/workflow.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/v1/internal/workflow/callback |
X-Internal-Token |
jBPM KIE Server posts process events (SLA breach, process completion). |
POST /api/v1/internal/workflow/callback — header X-Internal-Token (required).
Body WorkflowCallbackDTO:
| Field | Type | Required | Notes |
|---|---|---|---|
ticketId |
long | yes | The ticket the event relates to. |
eventType |
string | yes | SLA_BREACHED or PROCESS_COMPLETED. |
processInstanceId |
long | no | jBPM process instance ID. |
additionalData |
string | no | Free-text payload. |
POST /api/v1/internal/workflow/callback
X-Internal-Token: <shared-secret>
{
"ticketId": 42,
"eventType": "SLA_BREACHED",
"processInstanceId": 1001,
"additionalData": "SLA deadline was 2026-05-21T17:00:00Z"
}Returns 200 with a plain-text body on success; 400 for an unknown eventType,
401 for a missing/invalid token, 404 if the ticket does not exist.
| Resource | Endpoints |
|---|---|
| Tickets | 20 |
| Ticket Comments | 2 |
| Config | 1 |
| Admin Mail | 1 |
| Attachments | 4 |
| Worklogs | 5 |
| CSAT | 3 |
| Users | 23 |
| Notifications | 6 |
| Notification Preferences | 2 |
| Products | 6 |
| Ticket Topics | 4 |
| Known Issues | 5 |
| Canned Responses | 7 |
| Agent-Product Limits | 3 |
| Dashboard Metrics | 14 |
| AI Summaries (llm-service) | 4 |
| Internal / Workflow | 2 |
| Total | 112 |