Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] — 2026-07-01

Schema refresh for Jobber's current GraphQL API. The read path was rebuilt
against the live schema (verified via introspection) and tested against a real
account; the write path is temporarily gated pending a redesign.

### Fixed
- **Read commands modernized to the current schema.** Rewrote all query
selections in `api/queries.py`:
- Money now read from `amounts { total, subtotal, paymentsTotal, invoiceBalance, ... }`
instead of the removed `totalAmount` / `amountPaid` / `balance` scalars.
- Status fields are the typed enums `invoiceStatus` / `quoteStatus` / `jobStatus`,
and list filters use `InvoiceStatusTypeEnum` / `QuoteStatusTypeEnum` / `JobStatusTypeEnum`.
- Single-record lookups take `EncodedId!` (was `ID!`).
- Line items read `totalPrice` (was `total`) under a `nodes { ... }` connection;
invoice→jobs and client `tags` / `phones` updated to their current connection shapes.
- Client phone reads `phone` (was `phoneNumber`).
- `invoices list --unpaid` now filters client-side by outstanding balance (the old
`status: "UNPAID"` value is not a valid enum).

### Changed
- **Write commands temporarily disabled** (`clients create/update/delete`,
`jobs create/update/complete`, `quotes create/send/approve`,
`invoices create/send`). Jobber's current schema reworked the write surface:
`invoiceSend` / `quoteSend` / `quoteApprove` / `jobComplete` no longer exist, and
the create mutations require nested inputs (`dueDetails`, `tax`, `lineItems`,
`propertyId`) the commands don't yet collect. These now exit with a clear message
instead of a cryptic GraphQL error, pending the write redesign (1.2.0).

### Added
- Schema-shape guard tests (`tests/api/test_queries_schema.py`) to prevent
regressions to removed fields.

## [1.0.0] — 2026-06-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "getjobber-cli"
version = "1.0.0"
version = "1.1.0"
description = "A portable Python CLI for the Jobber GraphQL API — originally built by DC Tree Cutting and Land Service for internal automation."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
129 changes: 79 additions & 50 deletions src/getjobber_cli/api/queries.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
"""Pre-built GraphQL queries for GetJobber API."""
"""Pre-built GraphQL queries for GetJobber API.

Field selections target the pinned schema version in api/client.py
(X-JOBBER-GRAPHQL-VERSION). Money now lives under `amounts { ... }`, statuses
are typed enums (invoiceStatus / quoteStatus / jobStatus), and related records
are connections (jobs.nodes, properties.nodes) rather than the old *Ids / *Amount
scalar fields. See CHANGELOG 1.1.0.
"""

# Client Queries
LIST_CLIENTS = """
Expand All @@ -11,6 +18,8 @@
companyName
email
phone
isCompany
isLead
createdAt
updatedAt
}
Expand All @@ -24,15 +33,15 @@
"""

GET_CLIENT = """
query GetClient($id: ID!) {
query GetClient($id: EncodedId!) {
client(id: $id) {
id
firstName
lastName
companyName
email
phone
phones { nodes { number smsAllowed } }
phones { number primary smsAllowed }
billingAddress {
street1
street2
Expand All @@ -41,7 +50,13 @@
postalCode
country
}
tags
isCompany
isLead
tags {
nodes {
label
}
}
createdAt
updatedAt
}
Expand All @@ -50,7 +65,7 @@

SEARCH_CLIENTS = """
query SearchClients($query: String!, $first: Int) {
clients(first: $first, filter: {search: $query}) {
clients(first: $first, searchTerm: $query) {
nodes {
id
firstName
Expand All @@ -66,13 +81,13 @@

# Job Queries
LIST_JOBS = """
query ListJobs($first: Int, $after: String, $status: String) {
query ListJobs($first: Int, $after: String, $status: JobStatusTypeEnum) {
jobs(first: $first, after: $after, filter: {status: $status}) {
nodes {
id
title
jobNumber
status
jobStatus
client {
id
firstName
Expand All @@ -81,6 +96,7 @@
}
startAt
endAt
total
createdAt
updatedAt
}
Expand All @@ -94,13 +110,13 @@
"""

GET_JOB = """
query GetJob($id: ID!) {
query GetJob($id: EncodedId!) {
job(id: $id) {
id
title
jobNumber
status
description
jobStatus
instructions
client {
id
firstName
Expand All @@ -120,7 +136,7 @@
}
startAt
endAt
totalAmount
total
createdAt
updatedAt
}
Expand All @@ -129,21 +145,22 @@

# Quote Queries
LIST_QUOTES = """
query ListQuotes($first: Int, $after: String, $status: String) {
query ListQuotes($first: Int, $after: String, $status: QuoteStatusTypeEnum) {
quotes(first: $first, after: $after, filter: {status: $status}) {
nodes {
id
quoteNumber
title
status
quoteStatus
client {
id
firstName
lastName
companyName
}
totalAmount
sentAt
amounts {
total
}
createdAt
updatedAt
}
Expand All @@ -157,12 +174,12 @@
"""

GET_QUOTE = """
query GetQuote($id: ID!) {
query GetQuote($id: EncodedId!) {
quote(id: $id) {
id
quoteNumber
title
status
quoteStatus
message
client {
id
Expand All @@ -172,17 +189,21 @@
email
}
lineItems {
id
name
description
quantity
unitPrice
nodes {
id
name
description
quantity
unitPrice
totalPrice
}
}
amounts {
subtotal
discountAmount
taxAmount
total
}
subtotal
taxAmount
totalAmount
sentAt
createdAt
updatedAt
}
Expand All @@ -191,24 +212,26 @@

# Invoice Queries
LIST_INVOICES = """
query ListInvoices($first: Int, $after: String, $status: String) {
query ListInvoices($first: Int, $after: String, $status: InvoiceStatusTypeEnum) {
invoices(first: $first, after: $after, filter: {status: $status}) {
nodes {
id
invoiceNumber
subject
status
invoiceStatus
client {
id
firstName
lastName
companyName
}
totalAmount
amountPaid
balance
amounts {
total
paymentsTotal
invoiceBalance
}
dueDate
sentAt
issuedDate
createdAt
updatedAt
}
Expand All @@ -222,12 +245,12 @@
"""

GET_INVOICE = """
query GetInvoice($id: ID!) {
query GetInvoice($id: EncodedId!) {
invoice(id: $id) {
id
invoiceNumber
subject
status
invoiceStatus
message
client {
id
Expand All @@ -236,27 +259,33 @@
companyName
email
}
job {
id
title
jobNumber
jobs {
nodes {
id
title
jobNumber
}
}
lineItems {
id
name
description
quantity
unitPrice
nodes {
id
name
description
quantity
unitPrice
totalPrice
}
}
amounts {
subtotal
discountAmount
taxAmount
paymentsTotal
invoiceBalance
total
}
subtotal
taxAmount
totalAmount
amountPaid
balance
dueDate
sentAt
paidAt
issuedDate
createdAt
updatedAt
}
Expand Down
8 changes: 6 additions & 2 deletions src/getjobber_cli/commands/client_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from getjobber_cli.constants import DEFAULT_ITEMS_PER_PAGE, OUTPUT_FORMAT_TABLE
from getjobber_cli.utils.config import get_config
from getjobber_cli.utils.errors import GraphQLError, NotAuthenticatedError
from getjobber_cli.utils.gating import write_command_pending
from getjobber_cli.utils.formatters import (
extract_list_data,
extract_single_data,
Expand Down Expand Up @@ -62,7 +63,7 @@ def list_clients(
"Name": f"{c.get('firstName', '')} {c.get('lastName', '')}".strip()
or c.get("companyName", ""),
"Email": c.get("email", ""),
"Phone": c.get("phoneNumber", ""),
"Phone": c.get("phone", ""),
}
for c in clients
]
Expand Down Expand Up @@ -117,6 +118,7 @@ def get_client(
raise typer.Exit(1)


@write_command_pending
def create_client(
first_name: Annotated[Optional[str], typer.Option(help="First name")] = None,
last_name: Annotated[Optional[str], typer.Option(help="Last name")] = None,
Expand Down Expand Up @@ -186,6 +188,7 @@ def create_client(
raise typer.Exit(1)


@write_command_pending
def update_client(
client_id: Annotated[str, typer.Argument(help="Client ID")],
first_name: Annotated[Optional[str], typer.Option(help="First name")] = None,
Expand Down Expand Up @@ -246,6 +249,7 @@ def update_client(
raise typer.Exit(1)


@write_command_pending
def delete_client(
client_id: Annotated[str, typer.Argument(help="Client ID")],
force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation")] = False,
Expand Down Expand Up @@ -313,7 +317,7 @@ def search_clients(
"Name": f"{c.get('firstName', '')} {c.get('lastName', '')}".strip()
or c.get("companyName", ""),
"Email": c.get("email", ""),
"Phone": c.get("phoneNumber", ""),
"Phone": c.get("phone", ""),
}
for c in clients
]
Expand Down
Loading
Loading