The service job behind an order, for the admin console.
Addressed by **order** id rather than request id because that is what
the console has: service jobs are listed through adminListOrders,
which returns OrderEntity rows, and nothing exposed the way back to
the request they were created from. requests.order_id has been
written by selectOffer since orders existed and
find_by_order_id has been on the repo since the cross-vertical
cancellation flow needed it — only the read was missing.
Returns null for an order with no service request behind it: an order
of another kind, or one whose request was hard-deleted. That is a
legitimate answer for a console that lists orders by kind and can be
pointed at any id, not an error worth raising.
Separate from serviceRequest(id:), which is scoped to the customer
who raised the request and the providers bidding on it, and redacts the
address for a provider who has not paid to unlock it. An admin is
neither of those parties, so that query answers NotFound for them.
Every question on a service, for the console's question builder.
Named adminServiceQuestions, not serviceQuestions, because
BrowseQuery already owns that name. Two resolvers declaring the same
field on a MergedObject do not conflict loudly — one simply wins the
merge, and the browse one did, so this query has been unreachable since
it was written and the console had no way to read a *disabled* question
or an isEnabled flag at all.
The difference is not only the flag. BrowseQuestionResponse is the
customer's view of a question; this returns the whole row, which is what
an editor needs.
Open leads I can bid on.
serviceId narrows the board to one of my services. Omit it for the
union across everything I offer — the provider home feed wants that,
and used to fake it with one query per service stitched together on the
client, which skewed paging whenever one of them failed.
My own response to one request — a quote, a call request, or a request
for more information — or null if I have not responded.
A provider's job screen needs their own offer beside the request: its
status picks the primary action, and startedAt is what separates a
booked job from one under way. Nothing addressed it by request, so the
client paged myOffers looking for a match and stopped after three
pages — past which a busy provider's own bid simply disappeared.
List my submitted offers.
Takes pagination and returns page_info like every other list on the
platform. It used to take bare limit/offset and return a plain
array, which left the client with no total and no way to know whether
another page existed.
Jobs per day for the signed-in provider, for the home week strip.
Days with no jobs are absent rather than returned as zero — the client
knows which dates it asked for.
Leads I dismissed with skipRequest, newest dismissal first.
availableRequests filters these out, so the Skipped tab had no source
at all and could only show what the current session happened to
remember. unskipRequest puts one back on the board.
Every question asked about one request, oldest first.
Readable by the customer who raised the request and by any provider who
has responded to it. The provider's ask screen used to keep a
session-local list that emptied on every reopen; the customer had no
list at all.
List offers for a service request (customer only).
Bids on a request, ordered by sort.
Defaults to RECOMMENDED, which uses the same scorer Quick Match uses —
so the list the customer sees and the choice made on their behalf cannot
disagree.
Search live services by name or description, across every category.
browse_services is category-scoped, so a customer could only find a
trade by knowing which category it sits under. A query shorter than two
characters returns nothing rather than most of the catalogue.
Every extended-fee request on a job, newest first.
Includes the refused and withdrawn ones on purpose: they are the record
of what was asked for, and a customer looking at a job they argued about
should be able to see the argument.
Readable by the job's customer and by the provider who holds it.
How many providers offer this service within reach of this location.
Separate from the list so the header count does not pay to build and
sort rows the screen never renders.
One service request, by id.
Readable by the customer who raised it and by a provider who has
responded to it or can still bid on it; the address stays redacted for
a provider who has neither unlocked the lead nor won the job.
Both apps needed this and neither had it: the customer's detail screen
was paging through myServiceRequests looking for one id, and the
provider's job detail could not be rendered at all once a job left the
open lead board.
Finish a card payment the customer had to authenticate (3DS).
payServiceRequest hands back a client secret when the bank wants a
challenge and leaves the job awaiting payment. This is what the client
calls once the challenge is done — without it the customer authenticated
and the booking stayed unpaid for ever.
Safe to call twice: a payment already captured returns the job as it
stands rather than charging again.
The customer's answer.
Approving is the only thing that moves a job's total after the customer
has been shown it, so it is deliberately a separate call from reading
the request — and it is idempotent: the second answer is refused rather
than applied.
Ask the tenant's default LLM for a starter set of intake
questions for a new service category. The admin reviews, edits,
and accepts; this mutation does NOT persist the questions.
Admin-only. Requires the tenant to have serviceQuestionGenEnabled
turned on under Settings → AI Features and at least one
configured AI provider with a remaining monthly budget.
Ask the customer to agree to an extended fee on a job in progress.
One at a time per job: a second request while the first is unanswered is
refused, because the customer would otherwise be agreeing to one of two
live figures.
Select an offer for a service request.
Select an offer for a service request.
couponCode is optional and forgiving: a code that is invalid, expired,
not applicable or under its minimum is ignored and the booking goes
through at full price. Refusing the whole selection over a coupon would
strand a customer who has already chosen and a provider who has already
paid a credit to be chosen.
Test a push provider. Without device_token it just verifies the
credentials authenticate with Google; with a device_token it sends a
real test notification to that device for an end-to-end check.
Spend credits to see a lead's address, photos and contact details
before quoting for it. Safe to call more than once — an already-open
lead is not charged again.
Take back a bid the customer has not chosen yet.
The credit it cost is **not** refunded: unlocking a lead reveals the
address and contact details, and a refunded withdrawal would make
"bid, read the lead, withdraw" a free way to browse the board.
An accepted offer cannot be withdrawn — that is a booked job with an
order behind it, and leaving one goes through cancellation.
Arguments
offerId!
Returns!
Build the foundation once. Expand without limits.
BetterSuite is built for teams who see on-demand as a business — not a feature.