diff --git a/src/resources/tickets.ts b/src/resources/tickets.ts index c637954..cd17087 100644 --- a/src/resources/tickets.ts +++ b/src/resources/tickets.ts @@ -53,13 +53,32 @@ export class TicketsResource { // Build filter criteria from params const filters: Array<{ field: string; operator: string; value: unknown }> = []; if (params?.status) { - filters.push({ field: 'status', operator: 'is', value: params.status }); + // NinjaOne's filter API expects the numeric parent statusId, not the enum string. + // Custom tenant statuses inherit one of these via parentId. + // 1000/2000/3000/6000 confirmed via live tenant; 4000/5000/7000 inferred from + // NinjaOne docs and the 1000-stride pattern — verify against your tenant if you + // depend on filtering by IN_PROGRESS / RESOLVED / ON_HOLD. + const STATUS_PARENT_ID: Record = { + NEW: 1000, + OPEN: 2000, + WAITING: 3000, + IN_PROGRESS: 4000, + RESOLVED: 5000, + CLOSED: 6000, + ON_HOLD: 7000, + }; + const id = STATUS_PARENT_ID[params.status]; + if (id !== undefined) { + filters.push({ field: 'status', operator: 'is', value: id }); + } } if (params?.priority) { filters.push({ field: 'priority', operator: 'is', value: params.priority }); } if (params?.organizationId) { - filters.push({ field: 'organizationId', operator: 'is', value: params.organizationId }); + // Ticket payloads carry `clientId`, not `organizationId` — the filter field + // must match the ticket column name or the API returns 400. + filters.push({ field: 'clientId', operator: 'is', value: params.organizationId }); } if (params?.deviceId) { filters.push({ field: 'nodeId', operator: 'is', value: params.deviceId }); diff --git a/src/types/tickets.ts b/src/types/tickets.ts index 1ae379f..1bd6f47 100644 --- a/src/types/tickets.ts +++ b/src/types/tickets.ts @@ -81,7 +81,11 @@ export interface Ticket extends TimestampFields { * with filters and pagination in the request body. */ export interface TicketListParams { - /** Board ID to query (default: 1, typically the "All Tickets" board) */ + /** + * Board ID to query. Defaults to 1, which on most tenants is the "All Tickets" + * board — but this is not guaranteed. Call `listBoards()` to discover the + * board IDs for the current tenant if you need a specific board. + */ boardId?: number; /** Number of results per page (default: 50) */ pageSize?: number; diff --git a/tests/integration/tickets.test.ts b/tests/integration/tickets.test.ts index b26edfe..771328e 100644 --- a/tests/integration/tickets.test.ts +++ b/tests/integration/tickets.test.ts @@ -3,8 +3,11 @@ */ import { describe, it, expect } from 'vitest'; +import { http, HttpResponse } from 'msw'; import { NinjaOneClient } from '../../src/client.js'; import { NinjaOneNotFoundError } from '../../src/errors.js'; +import { server } from '../mocks/server.js'; +import * as ticketFixtures from '../fixtures/tickets.js'; describe('TicketsResource', () => { const client = new NinjaOneClient({ @@ -20,6 +23,61 @@ describe('TicketsResource', () => { expect(result.tickets).toHaveLength(2); expect(result.totalCount).toBe(2); }); + + it('should map status enum to parent statusId in filter body', async () => { + let capturedBody: Record | undefined; + server.use( + http.post( + 'https://app.ninjarmm.com/api/v2/ticketing/trigger/board/:boardId/run', + async ({ request }) => { + capturedBody = (await request.json()) as Record; + return HttpResponse.json(ticketFixtures.list); + } + ) + ); + + await client.tickets.list({ boardId: 2, status: 'OPEN' }); + + expect(capturedBody?.filters).toEqual([ + { field: 'status', operator: 'is', value: 2000 }, + ]); + }); + + it('should map organizationId filter to clientId field', async () => { + let capturedBody: Record | undefined; + server.use( + http.post( + 'https://app.ninjarmm.com/api/v2/ticketing/trigger/board/:boardId/run', + async ({ request }) => { + capturedBody = (await request.json()) as Record; + return HttpResponse.json(ticketFixtures.list); + } + ) + ); + + await client.tickets.list({ boardId: 2, organizationId: 42 }); + + expect(capturedBody?.filters).toEqual([ + { field: 'clientId', operator: 'is', value: 42 }, + ]); + }); + + it('should forward lastCursorId for pagination', async () => { + let capturedBody: Record | undefined; + server.use( + http.post( + 'https://app.ninjarmm.com/api/v2/ticketing/trigger/board/:boardId/run', + async ({ request }) => { + capturedBody = (await request.json()) as Record; + return HttpResponse.json(ticketFixtures.list); + } + ) + ); + + await client.tickets.list({ boardId: 2, lastCursorId: 50 }); + + expect(capturedBody?.lastCursorId).toBe(50); + }); }); describe('get', () => {