confirmJobRequest
Confirm a booking on behalf of the customer
Fires the customer-actor confirm_booking action from the BUSINESS surface (audited as business_on_behalf). Two uses: (1) LIVE — staff confirm a slot for a customer who booked by phone; (2) SANDBOX — the customer magic-token surface is live-only (a sandbox job's link can never reach a real customer), so this is the ONLY way to drive a sandbox test job past booking (book → quote → confirm → assign → complete). Body carries the customer-chosen scheduled_at (business-local naive datetime).
DECISION TABLE — every 409 this endpoint returns, and the correct NEXT STEP (branch on error_code, never on the HTTP status):
• JOB_REQUEST_STAGE_CONFLICT — the job changed since you read it (NOTE: every FAILED confirm attempt also bumps status_version by design). Next: re-GET the job, retry with the fresh status_version.
• JOB_REQUEST_ACTION_NOT_PENDING — the job is no longer at the confirm step (usually: already confirmed). Next: re-GET and show current status; do not retry.
• JOB_REQUEST_NO_TECHNICIAN_AVAILABLE — the TIME is infeasible for everyone (outside working hours / the customer window, or nobody qualifies). Best-effort data breakdown: considered (roster size checked), blocked_by (histogram of blockers[0].kind → count, only kinds that actually blocked someone), truncated (roster larger than the check covered). Next: pick another time via booking-windows / time-segments. NOT an emergency case — displacement cannot conjure capacity.
• JOB_REQUEST_TECH_INFEASIBLE — the FORCED technician cannot take the job then; data.reason says why (blockers[0].kind): outside_service_area | missing_required_skills | not_lead_tier | no_working_day | on_time_off | off_shift | calendar_busy | calendar_conflict | visit_too_long | occupied (they are already working then — data.earliest_feasible_at is when they are free) | cannot_arrive_in_time (see data.earliest_feasible_at, RFC3339 UTC — the first same-day time they CAN be on site → offer it) | not_available_today (diagnosis unavailable). data.blockers[] names EVERY cause, most-structural first; data.technician carries {id, name}, name best-effort (empty only when the lookup itself could not resolve) — EXCEPT on not_lead_tier, which carries a flat data.technician_id and no technician object. A UI that reads only reason still works. Next: keep the tech and reschedule to earliest_feasible_at+, OR keep the time and drop technician_id (auto-pick) / choose another tech from time-segments. NOT an emergency case.
• JOB_REQUEST_P0_REQUIRES_DISPLACEMENT — the ONLY code that routes to the EMERGENCY flow: the job is P0, the tech qualifies, but the lane is genuinely occupied. Next: POST emergency/candidates → preview → commit (the commit auto-confirms). Caveat: if the occupying jobs are themselves P0 the preview will reject with EMERGENCY_RESCHEDULE_SLOT_OCCUPIED (P0 never displaces P0) — then pick another tech/time.
Tool call template
Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.
{
"id": 1,
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"arguments": {
"id": "<id>"
},
"name": "confirmJobRequest"
}
}Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| id | Yes | Job request ID | |
| scheduled_at | No | Chosen start time — business-local wall clock (the business_time.datetime value from the time-segments picker), converted to UTC against the job's business timezone. Seconds may be omitted and a space may replace the T. An offset is accepted only when it agrees with the business timezone; a disagreeing one is refused with JOB_REQUEST_INVALID_INPUT naming what it means locally. | |
| technician_id | No | TechnicianID (BUSINESS confirm only — ignored on the customer surface): force-assign the job to this technician instead of the ranked auto-pick. Ranking is bypassed; feasibility (hours/time-off/geo/skills), the TierLead rule and the double-booking guard still apply — an infeasible forced tech rejects the confirm (P0 gets the displacement hint). ⚠️ One exception: for a SINGLE-PERSON p0 job, a PERSONAL-CALENDAR overlap on this exact technician no longer rejects — naming them here is treated as having already phoned them, so the confirm succeeds and the override is recorded on the business's activity feed (this response has no warnings array to carry it). A multi-member crew still rejects on it. | |
| status_version | No | Optimistic-lock fence: the status_version from your last read. Omitted/0 = fence on the row's current version (no race protection). | |
| idempotency_key | No | Unique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header) | |
| after_hours_override | No | AfterHoursOverride (BUSINESS confirm only): schedule this P0 outside the technician's working hours or approved time-off — you have phoned them and they agreed. Requires priority p0, a single-person job, and technician_id. Double-booking, service area, required skills and the lead-tier rule still reject. Deliberately NOT a binding-tag rule: gin evaluates tags before the handler, which would make the three AFTER_HOURS codes unreachable. | |
| arrival_window_minutes | No | ArrivalWindowMinutes = width (phút) của arrival-window ô khách bấm ở slot-picker (chính là time_slot_step_minutes, mặc định 30). Persist để post-confirm detail render lại đúng window. Optional; bounds ([5, 240], khớp slot-picker step) validate ở usecase — single authority, một error code (JOB_REQUEST_INVALID_INPUT). |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| data | No | The response payload. Omitted on error unless the error carries structured data. | |
| errors | No | Field-level validation details; present only for VALIDATION_ERROR responses. | |
| message | No | Human-readable message ("Success" or an error description), localized via the X-Locale header. | |
| error_code | No | 0 on success; a stable string error code on failure (e.g. CUSTOMER_NOT_FOUND). |