View Categories

Maintenance Tickets

3 min read

A maintenance ticket takes kit off the rentable fleet while it is being worked on. That is the whole point of it: a camera in the workshop must not be bookable, and the moment the ticket is closed it must be bookable again. These fields let you list, open, change and delete tickets, and they apply exactly the same fleet side effects the admin ticket screen does.

They come from the Maintenance add-on (SalesIgniter_Maintenance 1.2.48 or later), not the base rental extension. All of them need a token holding SalesIgniter_Maintenance::tickets.

What a ticket actually holds #

Worth understanding before you write one, because a ticket holds stock in two different ways:

  • Quantity. An open ticket places a reservation for its quantity, so that many units come off the calendar. This is what holds stock for a product that is not serial-tracked.
  • Named serials. If you list serials, those specific units are marked as held by the ticket and stop being rentable individually.

“Open” means the ticket’s status reserves inventory — that is a flag on each row in your maintenance status list, not a hard-coded set of names. Moving a ticket to a status that does not reserve inventory (a “Complete” status, typically) releases both the quantity and the serials. That is how you close a ticket; there is no separate close mutation.

Opening a ticket #

mutation SendToWorkshop {
  createRentalMaintenanceTicket(input: {
    sku: "CANON-R6"
    quantity: 1
    serials: ["CR6-0011"]
    summary: "Sensor cleaning + shutter count check"
    description: "Customer reported dust spots on returned images."
    maintainer_id: 3
    start_date: "2026-10-06 09:00:00"
    end_date: "2026-10-09 17:00:00"
    cost: 120.00
  }) {
    ticket {
      id
      sku
      quantity
      serials
      status_id
      status_label
      maintainer_name
      start_date
      end_date
      date_added
      added_from
    }
  }
}
  • status_id defaults to your first configured maintenance status, date_added to now, and added_from to "graphql" so you can tell API-created tickets from ones raised in the admin or by the automated-maintenance cron.
  • quantity cannot exceed the product’s total rental quantity — you cannot take more units off the fleet than exist.
  • Every serial is validated. It must exist for that product, must not already be on another open ticket, and must not be out on a rental. A unit that is with a customer cannot simultaneously be in your workshop, and accepting that would quietly corrupt availability.
  • end_date must be on or after start_date.

One difference from the admin worth knowing: the admin ticket screen does not re-validate serials on save — it relies on its picker only offering free ones, so a hand-crafted POST can put a rented-out unit on a ticket. This API validates on the server. If you are comparing behaviour and the admin lets something through that the API refuses, the API is the stricter and more correct of the two.

Finding tickets #

query WhatIsInTheWorkshop {
  rentalMaintenanceTickets(
    filter: { status_id: { in: [1, 2] } }
    sort: { start_date: ASC }
    pageSize: 50
  ) {
    total_count
    items {
      id
      sku
      product_name
      quantity
      serials
      summary
      status_label
      maintainer_name
      start_date
      end_date
    }
    page_info { current_page total_pages }
  }
}

Filter on product_id, sku, status_id and maintainer_id (each {eq} or {in}), on start_date, end_date and date_added as {from, to} ranges, and on serial as a {match} substring — which is how you answer “has this unit been in before?”. Sort on id, date_added, start_date, end_date or status_id; newest first by default. Fetch one with rentalMaintenanceTicket(id: 91).

status_label, maintainer_name, sku and product_name come back null rather than erroring if the underlying status, technician or product row has since been deleted — old tickets stay readable.

Updating and closing #

Only the fields you send are written. Closing a ticket is a status change:

mutation CloseIt {
  updateRentalMaintenanceTicket(
    id: 91
    input: {
      status_id: 4
      cost: 145.00
      comments: "Sensor cleaned, shutter at 48k, returned to fleet 2026-10-08."
    }
  ) {
    ticket { id status_id status_label serials cost }
  }
}

If status 4 is one that does not reserve inventory, the held quantity and every serial on the ticket go straight back into the fleet and the product becomes bookable again for those dates.

serials on update replaces the whole list rather than adding to it, and the fleet is reconciled against the change: units you dropped go back in, units you added come out. Sending a partial list by mistake therefore releases the ones you left out, so always send the full intended list.

The product a ticket is against cannot be changed, because the serials are product-scoped. Delete the ticket and open another.

Deleting #

mutation Remove {
  deleteRentalMaintenanceTicket(id: 91) { success id }
}

Deleting releases the held quantity and all held serials. Prefer closing a ticket by status to deleting it — a closed ticket keeps the maintenance history and cost against that unit, which is what makes serial filtering useful later. Delete is for tickets raised in error.

Tying it back to the fleet #

A unit held by a ticket reports status: MAINTENANCE and a maintenance_ticket_id in rentalSerialNumbers, so you can go in either direction: from a serial to the work being done on it, or from a ticket to the units it holds.

query WhyIsThisUnitUnavailable {
  rentalSerialNumbers(filter: { serial_number: { match: "CR6-0011" } }) {
    items { serial_number status maintenance_ticket_id reservation_id }
  }
}