crisphive 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +183 -16
  3. data/crisphive.gemspec +5 -5
  4. data/lib/crisphive/api/business_skill_api.rb +502 -8
  5. data/lib/crisphive/api/customer_api.rb +20 -14
  6. data/lib/crisphive/api/job_request_business_api.rb +1273 -167
  7. data/lib/crisphive/api/job_types_api.rb +214 -2
  8. data/lib/crisphive/api/permission_api.rb +79 -0
  9. data/lib/crisphive/api/service_area_api.rb +214 -6
  10. data/lib/crisphive/api/technician_api.rb +510 -6
  11. data/lib/crisphive/api/technician_time_off_api.rb +240 -0
  12. data/lib/crisphive/api/vehicle_api.rb +214 -6
  13. data/lib/crisphive/api/webhooks_api.rb +213 -0
  14. data/lib/crisphive/api_client.rb +2 -2
  15. data/lib/crisphive/api_error.rb +2 -2
  16. data/lib/crisphive/configuration.rb +3 -3
  17. data/lib/crisphive/models/api_internal_techtimeoff_actions.rb +232 -0
  18. data/lib/crisphive/models/api_internal_techtimeoff_status.rb +42 -0
  19. data/lib/crisphive/models/assignable_group.rb +289 -0
  20. data/lib/crisphive/models/book_and_confirm_job_request200_response.rb +253 -0
  21. data/lib/crisphive/models/commit_absence_resolve200_response.rb +253 -0
  22. data/lib/crisphive/models/commit_emergency_reschedule200_response.rb +253 -0
  23. data/lib/crisphive/models/commit_job_request_move200_response.rb +253 -0
  24. data/lib/crisphive/models/create_customer200_response.rb +5 -2
  25. data/lib/crisphive/models/create_job_request200_response.rb +5 -2
  26. data/lib/crisphive/models/create_job_type200_response.rb +253 -0
  27. data/lib/crisphive/models/create_service_area200_response.rb +253 -0
  28. data/lib/crisphive/models/create_skill200_response.rb +253 -0
  29. data/lib/crisphive/models/create_skill_category200_response.rb +253 -0
  30. data/lib/crisphive/models/create_technician200_response.rb +253 -0
  31. data/lib/crisphive/models/create_technician_time_off200_response.rb +253 -0
  32. data/lib/crisphive/models/create_vehicle200_response.rb +253 -0
  33. data/lib/crisphive/models/create_webhook_endpoint200_response.rb +253 -0
  34. data/lib/crisphive/models/customer.rb +13 -3
  35. data/lib/crisphive/models/customer_address.rb +2 -2
  36. data/lib/crisphive/models/customer_address_request.rb +11 -2
  37. data/lib/crisphive/models/customer_contact.rb +27 -7
  38. data/lib/crisphive/models/customer_create_request.rb +14 -4
  39. data/lib/crisphive/models/customer_create_response.rb +2 -2
  40. data/lib/crisphive/models/customer_list.rb +3 -3
  41. data/lib/crisphive/models/customer_list_item.rb +13 -3
  42. data/lib/crisphive/models/customer_profile.rb +2 -2
  43. data/lib/crisphive/models/customer_quick_books_link.rb +235 -0
  44. data/lib/crisphive/models/customer_service_area_ref.rb +225 -0
  45. data/lib/crisphive/models/customer_spending.rb +2 -2
  46. data/lib/crisphive/models/customer_technician_ref.rb +225 -0
  47. data/lib/crisphive/models/customer_tier_progress.rb +2 -2
  48. data/lib/crisphive/models/customer_update_request.rb +25 -22
  49. data/lib/crisphive/models/get_customer200_response.rb +5 -2
  50. data/lib/crisphive/models/get_job_request200_response.rb +5 -2
  51. data/lib/crisphive/models/get_job_request_timeline200_response.rb +5 -2
  52. data/lib/crisphive/models/get_job_type200_response.rb +5 -2
  53. data/lib/crisphive/models/get_service_area200_response.rb +5 -2
  54. data/lib/crisphive/models/get_technician200_response.rb +5 -2
  55. data/lib/crisphive/models/get_technician_schedule200_response.rb +253 -0
  56. data/lib/crisphive/models/get_vehicle200_response.rb +5 -2
  57. data/lib/crisphive/models/internal_jobrequest_http_address_req.rb +475 -0
  58. data/lib/crisphive/models/job_date.rb +5 -3
  59. data/lib/crisphive/models/job_date_business_range.rb +7 -3
  60. data/lib/crisphive/models/job_date_period.rb +2 -2
  61. data/lib/crisphive/models/job_date_period_entry.rb +4 -2
  62. data/lib/crisphive/models/job_request.rb +56 -6
  63. data/lib/crisphive/models/job_request_absence_alternative.rb +356 -0
  64. data/lib/crisphive/models/job_request_absence_alternative_cost.rb +275 -0
  65. data/lib/crisphive/models/job_request_absence_assignment.rb +319 -0
  66. data/lib/crisphive/models/job_request_absence_commit_request.rb +296 -0
  67. data/lib/crisphive/models/job_request_absence_notification.rb +283 -0
  68. data/lib/crisphive/models/job_request_absence_plan.rb +322 -0
  69. data/lib/crisphive/models/job_request_absence_preview_request.rb +280 -0
  70. data/lib/crisphive/models/job_request_absence_resolved_job.rb +499 -0
  71. data/lib/crisphive/models/job_request_absence_solver.rb +289 -0
  72. data/lib/crisphive/models/job_request_absence_time_off.rb +281 -0
  73. data/lib/crisphive/models/job_request_absence_unresolved_job.rb +364 -0
  74. data/lib/crisphive/models/job_request_action_audit_by.rb +2 -2
  75. data/lib/crisphive/models/job_request_action_audit_entry.rb +3 -3
  76. data/lib/crisphive/models/job_request_action_summary.rb +3 -3
  77. data/lib/crisphive/models/job_request_address_summary.rb +2 -2
  78. data/lib/crisphive/models/job_request_archive_summary.rb +2 -2
  79. data/lib/crisphive/models/job_request_arrival_window.rb +4 -4
  80. data/lib/crisphive/models/job_request_assigned_vehicle.rb +2 -2
  81. data/lib/crisphive/models/job_request_assignment_summary.rb +2 -2
  82. data/lib/crisphive/models/job_request_attention_summary.rb +259 -0
  83. data/lib/crisphive/models/job_request_available_vehicle.rb +235 -0
  84. data/lib/crisphive/models/job_request_blocker.rb +363 -0
  85. data/lib/crisphive/models/job_request_blocker_ref.rb +223 -0
  86. data/lib/crisphive/models/job_request_book_and_confirm_customer.rb +302 -0
  87. data/lib/crisphive/models/job_request_book_and_confirm_emergency.rb +315 -0
  88. data/lib/crisphive/models/job_request_book_and_confirm_emergency_result.rb +279 -0
  89. data/lib/crisphive/models/job_request_book_and_confirm_refusal.rb +280 -0
  90. data/lib/crisphive/models/job_request_book_and_confirm_request.rb +466 -0
  91. data/lib/crisphive/models/job_request_book_and_confirm_response.rb +295 -0
  92. data/lib/crisphive/models/job_request_booking_windows.rb +2 -2
  93. data/lib/crisphive/models/job_request_buddy_slot_candidates.rb +239 -0
  94. data/lib/crisphive/models/job_request_business_time_range.rb +3 -3
  95. data/lib/crisphive/models/job_request_changes.rb +2 -2
  96. data/lib/crisphive/models/job_request_confirm_request.rb +255 -0
  97. data/lib/crisphive/models/job_request_create_request.rb +61 -7
  98. data/lib/crisphive/models/job_request_create_response.rb +2 -2
  99. data/lib/crisphive/models/job_request_crew_candidates.rb +227 -0
  100. data/lib/crisphive/models/job_request_crew_change.rb +299 -0
  101. data/lib/crisphive/models/job_request_crew_member.rb +7 -4
  102. data/lib/crisphive/models/job_request_crew_member_input.rb +284 -0
  103. data/lib/crisphive/models/job_request_crew_recommendation.rb +255 -0
  104. data/lib/crisphive/models/job_request_customer_summary.rb +2 -2
  105. data/lib/crisphive/models/job_request_day_windows.rb +3 -3
  106. data/lib/crisphive/models/job_request_emergency_candidate.rb +319 -0
  107. data/lib/crisphive/models/job_request_emergency_candidates.rb +311 -0
  108. data/lib/crisphive/models/job_request_emergency_candidates_request.rb +341 -0
  109. data/lib/crisphive/models/job_request_emergency_commit_request.rb +361 -0
  110. data/lib/crisphive/models/job_request_emergency_inline_job.rb +353 -0
  111. data/lib/crisphive/models/job_request_emergency_plan.rb +367 -0
  112. data/lib/crisphive/models/job_request_emergency_preview_request.rb +342 -0
  113. data/lib/crisphive/models/job_request_job_date_business_range_request.rb +3 -3
  114. data/lib/crisphive/models/job_request_job_date_period_request.rb +2 -2
  115. data/lib/crisphive/models/job_request_job_date_request.rb +3 -3
  116. data/lib/crisphive/models/job_request_lead_candidate.rb +311 -0
  117. data/lib/crisphive/models/job_request_list.rb +2 -2
  118. data/lib/crisphive/models/job_request_move_commit_req.rb +353 -0
  119. data/lib/crisphive/models/job_request_move_member.rb +299 -0
  120. data/lib/crisphive/models/job_request_move_plan.rb +387 -0
  121. data/lib/crisphive/models/job_request_move_preview_req.rb +300 -0
  122. data/lib/crisphive/models/job_request_move_warning.rb +335 -0
  123. data/lib/crisphive/models/job_request_period.rb +2 -2
  124. data/lib/crisphive/models/job_request_priority_update_result.rb +217 -0
  125. data/lib/crisphive/models/job_request_quote_request.rb +343 -0
  126. data/lib/crisphive/models/job_request_quote_summary.rb +2 -2
  127. data/lib/crisphive/models/job_request_rating_summary.rb +2 -2
  128. data/lib/crisphive/models/job_request_reassign_fallback.rb +287 -0
  129. data/lib/crisphive/models/job_request_reschedule_day.rb +227 -0
  130. data/lib/crisphive/models/job_request_reschedule_move.rb +319 -0
  131. data/lib/crisphive/models/job_request_reschedule_reassignment.rb +339 -0
  132. data/lib/crisphive/models/job_request_schedule.rb +2 -2
  133. data/lib/crisphive/models/job_request_session.rb +8 -7
  134. data/lib/crisphive/models/job_request_skill_summary.rb +3 -3
  135. data/lib/crisphive/models/job_request_status_summary.rb +3 -3
  136. data/lib/crisphive/models/job_request_tech_candidate.rb +287 -0
  137. data/lib/crisphive/models/job_request_technician_info.rb +2 -2
  138. data/lib/crisphive/models/job_request_time_segments.rb +287 -0
  139. data/lib/crisphive/models/job_request_time_segments_business_time.rb +321 -0
  140. data/lib/crisphive/models/job_request_time_segments_day.rb +247 -0
  141. data/lib/crisphive/models/job_request_time_segments_slot.rb +375 -0
  142. data/lib/crisphive/models/job_request_time_segments_worker.rb +295 -0
  143. data/lib/crisphive/models/job_request_timeline.rb +2 -2
  144. data/lib/crisphive/models/job_request_timeline_action.rb +2 -2
  145. data/lib/crisphive/models/job_request_timeline_event.rb +2 -2
  146. data/lib/crisphive/models/job_request_update_priority_request.rb +295 -0
  147. data/lib/crisphive/models/job_type.rb +44 -4
  148. data/lib/crisphive/models/job_type_create_request.rb +399 -0
  149. data/lib/crisphive/models/job_type_create_response.rb +215 -0
  150. data/lib/crisphive/models/job_type_update_request.rb +400 -0
  151. data/lib/crisphive/models/list_assignable_groups200_response.rb +255 -0
  152. data/lib/crisphive/models/list_crew_candidates200_response.rb +253 -0
  153. data/lib/crisphive/models/list_customers200_response.rb +5 -2
  154. data/lib/crisphive/models/list_emergency_candidates200_response.rb +253 -0
  155. data/lib/crisphive/models/list_job_request_booking_windows200_response.rb +5 -2
  156. data/lib/crisphive/models/list_job_request_changes200_response.rb +5 -2
  157. data/lib/crisphive/models/list_job_requests200_response.rb +5 -2
  158. data/lib/crisphive/models/list_job_types200_response.rb +5 -2
  159. data/lib/crisphive/models/list_matching_slots200_response.rb +253 -0
  160. data/lib/crisphive/models/list_nearby_technicians200_response.rb +253 -0
  161. data/lib/crisphive/models/list_service_areas200_response.rb +5 -2
  162. data/lib/crisphive/models/list_skill_categories200_response.rb +5 -2
  163. data/lib/crisphive/models/list_skills200_response.rb +5 -2
  164. data/lib/crisphive/models/list_skills_by_category200_response.rb +5 -2
  165. data/lib/crisphive/models/list_technician_skills200_response.rb +253 -0
  166. data/lib/crisphive/models/list_technician_time_off200_response.rb +253 -0
  167. data/lib/crisphive/models/list_technicians200_response.rb +5 -2
  168. data/lib/crisphive/models/list_vehicles200_response.rb +5 -2
  169. data/lib/crisphive/models/list_webhook_event_types200_response.rb +255 -0
  170. data/lib/crisphive/models/nearby_technician.rb +319 -0
  171. data/lib/crisphive/models/nearby_technicians.rb +237 -0
  172. data/lib/crisphive/models/pagination.rb +7 -2
  173. data/lib/crisphive/models/quote_job_request200_response.rb +253 -0
  174. data/lib/crisphive/models/replace_technician_service_areas200_response.rb +253 -0
  175. data/lib/crisphive/models/response_envelope.rb +6 -2
  176. data/lib/crisphive/models/service_area.rb +23 -3
  177. data/lib/crisphive/models/service_area_create_request.rb +455 -0
  178. data/lib/crisphive/models/service_area_create_response.rb +215 -0
  179. data/lib/crisphive/models/service_area_list.rb +2 -2
  180. data/lib/crisphive/models/service_area_update_request.rb +451 -0
  181. data/lib/crisphive/models/skill.rb +9 -2
  182. data/lib/crisphive/models/skill_by_category_list.rb +4 -2
  183. data/lib/crisphive/models/skill_category.rb +7 -2
  184. data/lib/crisphive/models/skill_category_create_request.rb +270 -0
  185. data/lib/crisphive/models/skill_category_create_response.rb +215 -0
  186. data/lib/crisphive/models/skill_category_list.rb +4 -2
  187. data/lib/crisphive/models/skill_create_request.rb +270 -0
  188. data/lib/crisphive/models/skill_create_response.rb +215 -0
  189. data/lib/crisphive/models/skill_list.rb +2 -2
  190. data/lib/crisphive/models/skill_summary.rb +2 -2
  191. data/lib/crisphive/models/skill_update_request.rb +281 -0
  192. data/lib/crisphive/models/technician.rb +66 -4
  193. data/lib/crisphive/models/technician_address.rb +2 -2
  194. data/lib/crisphive/models/technician_address_input.rb +408 -0
  195. data/lib/crisphive/models/technician_buddies_request.rb +236 -0
  196. data/lib/crisphive/models/technician_create_request.rb +603 -0
  197. data/lib/crisphive/models/technician_create_response.rb +215 -0
  198. data/lib/crisphive/models/technician_lead_ref.rb +2 -2
  199. data/lib/crisphive/models/technician_leads_request.rb +236 -0
  200. data/lib/crisphive/models/technician_list.rb +3 -3
  201. data/lib/crisphive/models/technician_schedule.rb +279 -0
  202. data/lib/crisphive/models/technician_schedule_block.rb +225 -0
  203. data/lib/crisphive/models/technician_schedule_session.rb +311 -0
  204. data/lib/crisphive/models/technician_service_area_ref.rb +2 -2
  205. data/lib/crisphive/models/technician_service_areas_request.rb +236 -0
  206. data/lib/crisphive/models/technician_service_areas_response.rb +217 -0
  207. data/lib/crisphive/models/technician_skill.rb +265 -0
  208. data/lib/crisphive/models/technician_skill_list.rb +227 -0
  209. data/lib/crisphive/models/technician_skills_request.rb +236 -0
  210. data/lib/crisphive/models/technician_status.rb +2 -2
  211. data/lib/crisphive/models/technician_time_off.rb +334 -0
  212. data/lib/crisphive/models/technician_time_off_create_request.rb +285 -0
  213. data/lib/crisphive/models/technician_time_off_create_response.rb +215 -0
  214. data/lib/crisphive/models/technician_time_off_list.rb +246 -0
  215. data/lib/crisphive/models/technician_update_request.rb +507 -0
  216. data/lib/crisphive/models/technician_vehicles_request.rb +236 -0
  217. data/lib/crisphive/models/vehicle.rb +26 -4
  218. data/lib/crisphive/models/vehicle_create_request.rb +420 -0
  219. data/lib/crisphive/models/vehicle_create_response.rb +215 -0
  220. data/lib/crisphive/models/vehicle_list.rb +2 -2
  221. data/lib/crisphive/models/vehicle_owner.rb +2 -2
  222. data/lib/crisphive/models/vehicle_update_request.rb +440 -0
  223. data/lib/crisphive/models/webhook_create_request.rb +320 -0
  224. data/lib/crisphive/models/webhook_create_response.rb +391 -0
  225. data/lib/crisphive/models/webhook_verification.rb +235 -0
  226. data/lib/crisphive/version.rb +3 -3
  227. data/lib/crisphive.rb +126 -2
  228. data/spec/spec_helper.rb +2 -2
  229. metadata +129 -5
@@ -1,7 +1,7 @@
1
1
  =begin
2
- #CrispHive Developer API
2
+ #Crisphive Developer API
3
3
 
4
- #Public REST API for integrating CrispHive from your own backend. Authenticate every request with a secret API key as a Bearer token (`Authorization: Bearer chsk_live_…`). The key prefix selects the data environment: `chsk_live_…` → production (live), `chsk_test_…` → sandbox (isolated test). **Key scopes (restricted keys).** A key is either *full-access* (can call every endpoint below) or *restricted* to a set of permission codes chosen at creation — the same codes as the dashboard permission grid (e.g. `customers_view`, `job_create`, `team_manage`). A restricted key calling an endpoint outside its scope gets `403`. The full code list is the permission catalog (`GET /permission/modules` on the dashboard API). Create, scope, and revoke keys from the business dashboard. Every response is wrapped in the envelope `{ \"error_code\": 0, \"message\": \"Success\", \"data\": <payload> }`.
4
+ #Public REST API for integrating Crisphive from your own backend. Authenticate every request with a secret API key as a Bearer token (`Authorization: Bearer chsk_live_…`). The key prefix selects the data environment: `chsk_live_…` → production (live), `chsk_test_…` → sandbox (isolated test). **Key scopes (restricted keys).** A key is either *full-access* (can call every endpoint below) or *restricted* to a set of permission codes chosen at creation — the same codes as the dashboard permission grid (e.g. `customers_view`, `job_create`, `team_manage`). A restricted key calling an endpoint outside its scope gets `403`. The full code list is the permission catalog (`GET /permission/modules` on the dashboard API). Create, scope, and revoke keys from the business dashboard. Every response is wrapped in the envelope `{ \"error_code\": 0, \"message\": \"Success\", \"data\": <payload> }`.
5
5
 
6
6
  The version of the OpenAPI document: 1.0
7
7
 
@@ -19,31 +19,33 @@ module Crisphive
19
19
  def initialize(api_client = ApiClient.default)
20
20
  @api_client = api_client
21
21
  end
22
- # Create a job request (business actor)
23
- # @param job_request_create_request [JobRequestCreateRequest] Booking payload
22
+ # Book, schedule and confirm a job in one call
23
+ # Creates the job, quotes it (job_duration_minutes, or the job type's default_duration_minutes) and confirms it at scheduled_at on the customer's behalf — the dashboard's \"book for a caller\" in a single request, built for voice agents and automation platforms that cannot run create → quote → slots → confirm. Customer: send `customer_id`, or `customer` (+ `address`) to match-or-create by phone/email — a caller who already exists is matched, never duplicated. Find a caller first with listCustomers?phone=. Every input is validated BEFORE anything is written (time and business timezone, future start, a duration or a job-type default, non-negative mobilization/demobilization minutes, active job type, phone format); those failures are ordinary 4xx and create nothing. ⚠️ A `technician_id` the engine cannot honour at that time is refused BEFORE anything is written: `409 JOB_REQUEST_TECH_INFEASIBLE`, no job, no customer, no webhook. Change the technician or the time and send again with a NEW Idempotency-Key: like every 4xx here the 409 is cached against its key for 24h, so the same key replays it (same body) or answers 422 IDEMPOTENCY_KEY_REUSE (changed body). This check is BEST-EFFORT — if the scheduling engine cannot be reached it is skipped and the call proceeds, so a pinned request can still come back as the 200 + `refusal` below. Handle both. ⚠️ ONE carve-out: a `p0` whose pinned technician is merely BUSY is NOT refused here. The job is created and the call answers 200 + `refusal` carrying JOB_REQUEST_P0_REQUIRES_DISPLACEMENT, because the remedy for that code is the emergency flow and listEmergencyCandidates / previewEmergencyReschedule / commitEmergencyReschedule all require `emergency_job_id` — a 409 carries no job id. Read `job_id` off the 200 and run the emergency flow; do not re-book. Every other doomed pin on a `p0` (out of area, missing skills, not a lead) still answers 409. ⚠️ A SECOND carve-out, narrower: for a `p0`, a PERSONAL-CALENDAR overlap on the pinned technician is not refused here either — naming them is treated as having already phoned them — and the call answers 200 + `confirmed: true`. Nothing on this response discloses it; the only record is the business's activity feed (`job_calendar_override`). `calendar_busy`/`calendar_conflict` therefore never appear in the 409 reason vocabulary below when the pin is a `p0` naming a technician explicitly (this endpoint never books a crew, so the exclusion is unconditional here). ⚠️ Otherwise, once the job is created it is never discarded, and the call answers 200 even if scheduling then fails: `confirmed: false` with `refusal` (stage + the exact error_code/data the quote or confirm endpoint would have returned, e.g. JOB_REQUEST_NO_TECHNICIAN_AVAILABLE with blockers). The job is then quoted and waiting in the coordinator's queue — tell the caller the office will confirm a time. Retry with the SAME Idempotency-Key to replay the result; a new key books a second job. P0 dispatch: send `emergency` with `priority: \"p0\"`. `dispatch: \"now\"` plans the displacement cascade BEFORE anything is written — a refusal is a 409 creating nothing — then books, quotes and commits it. `dispatch: \"later\"` parks a quoted P0 in the tray and ignores the other fields. ⚠️ `emergency.expected_move_ids` is REQUIRED with `dispatch: \"now\"` and has three meanings: omitted is refused, `[]` means \"my preview showed no displacement\" and commits a free lane, and a named set is the fence. Get the ids from previewEmergencyReschedule. ⚠️ A refusal AFTER the job is created answers 200 with `confirmed: false` and `refusal.stage: \"dispatch\"`. Read `job_id`, re-preview with previewEmergencyReschedule and commit with commitEmergencyReschedule. Never re-book.
24
+ # @param job_request_book_and_confirm_request [JobRequestBookAndConfirmRequest] Book and confirm in one call
24
25
  # @param [Hash] opts the optional parameters
25
- # @option opts [String] :x_timezone Customer IANA timezone
26
- # @return [CreateJobRequest200Response]
27
- def create_job_request(job_request_create_request, opts = {})
28
- data, _status_code, _headers = create_job_request_with_http_info(job_request_create_request, opts)
26
+ # @option opts [String] :idempotency_key Makes retries safe: a repeat send with the same key returns the original result instead of booking again
27
+ # @return [BookAndConfirmJobRequest200Response]
28
+ def book_and_confirm_job_request(job_request_book_and_confirm_request, opts = {})
29
+ data, _status_code, _headers = book_and_confirm_job_request_with_http_info(job_request_book_and_confirm_request, opts)
29
30
  data
30
31
  end
31
32
 
32
- # Create a job request (business actor)
33
- # @param job_request_create_request [JobRequestCreateRequest] Booking payload
33
+ # Book, schedule and confirm a job in one call
34
+ # Creates the job, quotes it (job_duration_minutes, or the job type&#39;s default_duration_minutes) and confirms it at scheduled_at on the customer&#39;s behalf — the dashboard&#39;s \&quot;book for a caller\&quot; in a single request, built for voice agents and automation platforms that cannot run create → quote → slots → confirm. Customer: send &#x60;customer_id&#x60;, or &#x60;customer&#x60; (+ &#x60;address&#x60;) to match-or-create by phone/email — a caller who already exists is matched, never duplicated. Find a caller first with listCustomers?phone&#x3D;. Every input is validated BEFORE anything is written (time and business timezone, future start, a duration or a job-type default, non-negative mobilization/demobilization minutes, active job type, phone format); those failures are ordinary 4xx and create nothing. ⚠️ A &#x60;technician_id&#x60; the engine cannot honour at that time is refused BEFORE anything is written: &#x60;409 JOB_REQUEST_TECH_INFEASIBLE&#x60;, no job, no customer, no webhook. Change the technician or the time and send again with a NEW Idempotency-Key: like every 4xx here the 409 is cached against its key for 24h, so the same key replays it (same body) or answers 422 IDEMPOTENCY_KEY_REUSE (changed body). This check is BEST-EFFORT — if the scheduling engine cannot be reached it is skipped and the call proceeds, so a pinned request can still come back as the 200 + &#x60;refusal&#x60; below. Handle both. ⚠️ ONE carve-out: a &#x60;p0&#x60; whose pinned technician is merely BUSY is NOT refused here. The job is created and the call answers 200 + &#x60;refusal&#x60; carrying JOB_REQUEST_P0_REQUIRES_DISPLACEMENT, because the remedy for that code is the emergency flow and listEmergencyCandidates / previewEmergencyReschedule / commitEmergencyReschedule all require &#x60;emergency_job_id&#x60; — a 409 carries no job id. Read &#x60;job_id&#x60; off the 200 and run the emergency flow; do not re-book. Every other doomed pin on a &#x60;p0&#x60; (out of area, missing skills, not a lead) still answers 409. ⚠️ A SECOND carve-out, narrower: for a &#x60;p0&#x60;, a PERSONAL-CALENDAR overlap on the pinned technician is not refused here either — naming them is treated as having already phoned them — and the call answers 200 + &#x60;confirmed: true&#x60;. Nothing on this response discloses it; the only record is the business&#39;s activity feed (&#x60;job_calendar_override&#x60;). &#x60;calendar_busy&#x60;/&#x60;calendar_conflict&#x60; therefore never appear in the 409 reason vocabulary below when the pin is a &#x60;p0&#x60; naming a technician explicitly (this endpoint never books a crew, so the exclusion is unconditional here). ⚠️ Otherwise, once the job is created it is never discarded, and the call answers 200 even if scheduling then fails: &#x60;confirmed: false&#x60; with &#x60;refusal&#x60; (stage + the exact error_code/data the quote or confirm endpoint would have returned, e.g. JOB_REQUEST_NO_TECHNICIAN_AVAILABLE with blockers). The job is then quoted and waiting in the coordinator&#39;s queue — tell the caller the office will confirm a time. Retry with the SAME Idempotency-Key to replay the result; a new key books a second job. P0 dispatch: send &#x60;emergency&#x60; with &#x60;priority: \&quot;p0\&quot;&#x60;. &#x60;dispatch: \&quot;now\&quot;&#x60; plans the displacement cascade BEFORE anything is written — a refusal is a 409 creating nothing — then books, quotes and commits it. &#x60;dispatch: \&quot;later\&quot;&#x60; parks a quoted P0 in the tray and ignores the other fields. ⚠️ &#x60;emergency.expected_move_ids&#x60; is REQUIRED with &#x60;dispatch: \&quot;now\&quot;&#x60; and has three meanings: omitted is refused, &#x60;[]&#x60; means \&quot;my preview showed no displacement\&quot; and commits a free lane, and a named set is the fence. Get the ids from previewEmergencyReschedule. ⚠️ A refusal AFTER the job is created answers 200 with &#x60;confirmed: false&#x60; and &#x60;refusal.stage: \&quot;dispatch\&quot;&#x60;. Read &#x60;job_id&#x60;, re-preview with previewEmergencyReschedule and commit with commitEmergencyReschedule. Never re-book.
35
+ # @param job_request_book_and_confirm_request [JobRequestBookAndConfirmRequest] Book and confirm in one call
34
36
  # @param [Hash] opts the optional parameters
35
- # @option opts [String] :x_timezone Customer IANA timezone
36
- # @return [Array<(CreateJobRequest200Response, Integer, Hash)>] CreateJobRequest200Response data, response status code and response headers
37
- def create_job_request_with_http_info(job_request_create_request, opts = {})
37
+ # @option opts [String] :idempotency_key Makes retries safe: a repeat send with the same key returns the original result instead of booking again
38
+ # @return [Array<(BookAndConfirmJobRequest200Response, Integer, Hash)>] BookAndConfirmJobRequest200Response data, response status code and response headers
39
+ def book_and_confirm_job_request_with_http_info(job_request_book_and_confirm_request, opts = {})
38
40
  if @api_client.config.debugging
39
- @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.create_job_request ...'
41
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.book_and_confirm_job_request ...'
40
42
  end
41
- # verify the required parameter 'job_request_create_request' is set
42
- if @api_client.config.client_side_validation && job_request_create_request.nil?
43
- fail ArgumentError, "Missing the required parameter 'job_request_create_request' when calling JobRequestBusinessApi.create_job_request"
43
+ # verify the required parameter 'job_request_book_and_confirm_request' is set
44
+ if @api_client.config.client_side_validation && job_request_book_and_confirm_request.nil?
45
+ fail ArgumentError, "Missing the required parameter 'job_request_book_and_confirm_request' when calling JobRequestBusinessApi.book_and_confirm_job_request"
44
46
  end
45
47
  # resource path
46
- local_var_path = '/job-requests'
48
+ local_var_path = '/job-requests/book-and-confirm'
47
49
 
48
50
  # query parameters
49
51
  query_params = opts[:query_params] || {}
@@ -57,22 +59,22 @@ module Crisphive
57
59
  if !content_type.nil?
58
60
  header_params['Content-Type'] = content_type
59
61
  end
60
- header_params[:'X-Timezone'] = opts[:'x_timezone'] if !opts[:'x_timezone'].nil?
62
+ header_params[:'Idempotency-Key'] = opts[:'idempotency_key'] if !opts[:'idempotency_key'].nil?
61
63
 
62
64
  # form parameters
63
65
  form_params = opts[:form_params] || {}
64
66
 
65
67
  # http body (model)
66
- post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_create_request)
68
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_book_and_confirm_request)
67
69
 
68
70
  # return_type
69
- return_type = opts[:debug_return_type] || 'CreateJobRequest200Response'
71
+ return_type = opts[:debug_return_type] || 'BookAndConfirmJobRequest200Response'
70
72
 
71
73
  # auth_names
72
74
  auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
73
75
 
74
76
  new_options = opts.merge(
75
- :operation => :"JobRequestBusinessApi.create_job_request",
77
+ :operation => :"JobRequestBusinessApi.book_and_confirm_job_request",
76
78
  :header_params => header_params,
77
79
  :query_params => query_params,
78
80
  :form_params => form_params,
@@ -83,34 +85,38 @@ module Crisphive
83
85
 
84
86
  data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
85
87
  if @api_client.config.debugging
86
- @api_client.config.logger.debug "API called: JobRequestBusinessApi#create_job_request\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
88
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#book_and_confirm_job_request\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
87
89
  end
88
90
  return data, status_code, headers
89
91
  end
90
92
 
91
- # Get a job request
92
- # @param id [String] Job request ID (UUID or short_code)
93
+ # Commit the previewed re-staffing of a technician's day
94
+ # Applies the plan returned by /absence/preview, ATOMICALLY: every assignment in one transaction (all or nothing), each job re-staffed onto its alternate at its unchanged window, its \"needs attention\" flag cleared, its status_version bumped. Commit VERIFIES and never re-solves — send `assignments[]` copied from `preview.resolved[]` (you may drop rows, never add or re-point them) and, to ACCEPT a priced alternative from `preview.unresolved[].alternatives[]`, the same row plus its `alternative_kind` and (for a reschedule kind) its `start_at`/`end_at`; the engine re-checks that technician at that window under exactly that relaxation. A reschedule kind rewrites `scheduled_at`, notifies the customer of the NEW TIME (`job_rescheduled`, never `tech_reassigned` on top) and fires `job_request.rescheduled`; the response row then carries `alternative_kind`, `cost`, `original_start_at`/`original_end_at` and `window_preserved: false`. Requires a time-off record covering EVERY day of the range for the technician (pending or approved); a pending one is APPROVED by the commit, because the engine's feasibility filter reads approved time-off only and without it the absent technician stays bookable everywhere else. The response mirrors the preview plus per-job `notification` evidence (dispatched | skipped + reason — what the routing WILL do, never proof of delivery), `attention_cleared`, and `time_off`. Supports Idempotency-Key. Requires job_manage AND schedule_manage (the approval is a scheduling action). See ABSENCE_RESOLVE_DESIGN.md. 409 NEXT STEPS: ABSENCE_RESOLVE_TIME_OFF_REQUIRED — the absence is not recorded well enough to commit against: `data.uncovered_dates[]` (days no eligible record touches) and `data.uncovered_jobs[]` (jobs no single eligible record spans); eligible = approved records, plus PENDING records whose span lies INSIDE the range — a pending record WIDER than the range (somebody's leave request) is never approved by this commit and is listed in `data.pending_wider_time_off_ids[]` for a human to decide. Record a sick-day time-off via `data.create_via` (POST /business/technician-time-off, pending is enough), then commit again. ABSENCE_RESOLVE_PLAN_DRIFTED — the world moved since the preview; `data.drifted[]` names the job (job_id/short_code/expected_version) and `reason` says how: `version` (the row changed or left the technician's lane), `infeasible` (the alternate can no longer take it), `occupied` (the alternate's lane overlapped after the write). No job was written; `data.time_offs[]` lists any pending record the commit had already approved — re-preview, show the new plan, commit again. ABSENCE_RESOLVE_NO_ORPHANED_JOBS — the board is empty for the range.
95
+ # @param job_request_absence_commit_request [JobRequestAbsenceCommitRequest] preview scope + the previewed assignments to apply
93
96
  # @param [Hash] opts the optional parameters
94
- # @return [GetJobRequest200Response]
95
- def get_job_request(id, opts = {})
96
- data, _status_code, _headers = get_job_request_with_http_info(id, opts)
97
+ # @option opts [String] :idempotency_key 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.
98
+ # @return [CommitAbsenceResolve200Response]
99
+ def commit_absence_resolve(job_request_absence_commit_request, opts = {})
100
+ data, _status_code, _headers = commit_absence_resolve_with_http_info(job_request_absence_commit_request, opts)
97
101
  data
98
102
  end
99
103
 
100
- # Get a job request
101
- # @param id [String] Job request ID (UUID or short_code)
104
+ # Commit the previewed re-staffing of a technician&#39;s day
105
+ # Applies the plan returned by /absence/preview, ATOMICALLY: every assignment in one transaction (all or nothing), each job re-staffed onto its alternate at its unchanged window, its \&quot;needs attention\&quot; flag cleared, its status_version bumped. Commit VERIFIES and never re-solves — send &#x60;assignments[]&#x60; copied from &#x60;preview.resolved[]&#x60; (you may drop rows, never add or re-point them) and, to ACCEPT a priced alternative from &#x60;preview.unresolved[].alternatives[]&#x60;, the same row plus its &#x60;alternative_kind&#x60; and (for a reschedule kind) its &#x60;start_at&#x60;/&#x60;end_at&#x60;; the engine re-checks that technician at that window under exactly that relaxation. A reschedule kind rewrites &#x60;scheduled_at&#x60;, notifies the customer of the NEW TIME (&#x60;job_rescheduled&#x60;, never &#x60;tech_reassigned&#x60; on top) and fires &#x60;job_request.rescheduled&#x60;; the response row then carries &#x60;alternative_kind&#x60;, &#x60;cost&#x60;, &#x60;original_start_at&#x60;/&#x60;original_end_at&#x60; and &#x60;window_preserved: false&#x60;. Requires a time-off record covering EVERY day of the range for the technician (pending or approved); a pending one is APPROVED by the commit, because the engine&#39;s feasibility filter reads approved time-off only and without it the absent technician stays bookable everywhere else. The response mirrors the preview plus per-job &#x60;notification&#x60; evidence (dispatched | skipped + reason — what the routing WILL do, never proof of delivery), &#x60;attention_cleared&#x60;, and &#x60;time_off&#x60;. Supports Idempotency-Key. Requires job_manage AND schedule_manage (the approval is a scheduling action). See ABSENCE_RESOLVE_DESIGN.md. 409 NEXT STEPS: ABSENCE_RESOLVE_TIME_OFF_REQUIRED — the absence is not recorded well enough to commit against: &#x60;data.uncovered_dates[]&#x60; (days no eligible record touches) and &#x60;data.uncovered_jobs[]&#x60; (jobs no single eligible record spans); eligible &#x3D; approved records, plus PENDING records whose span lies INSIDE the range — a pending record WIDER than the range (somebody&#39;s leave request) is never approved by this commit and is listed in &#x60;data.pending_wider_time_off_ids[]&#x60; for a human to decide. Record a sick-day time-off via &#x60;data.create_via&#x60; (POST /business/technician-time-off, pending is enough), then commit again. ABSENCE_RESOLVE_PLAN_DRIFTED — the world moved since the preview; &#x60;data.drifted[]&#x60; names the job (job_id/short_code/expected_version) and &#x60;reason&#x60; says how: &#x60;version&#x60; (the row changed or left the technician&#39;s lane), &#x60;infeasible&#x60; (the alternate can no longer take it), &#x60;occupied&#x60; (the alternate&#39;s lane overlapped after the write). No job was written; &#x60;data.time_offs[]&#x60; lists any pending record the commit had already approved — re-preview, show the new plan, commit again. ABSENCE_RESOLVE_NO_ORPHANED_JOBS — the board is empty for the range.
106
+ # @param job_request_absence_commit_request [JobRequestAbsenceCommitRequest] preview scope + the previewed assignments to apply
102
107
  # @param [Hash] opts the optional parameters
103
- # @return [Array<(GetJobRequest200Response, Integer, Hash)>] GetJobRequest200Response data, response status code and response headers
104
- def get_job_request_with_http_info(id, opts = {})
108
+ # @option opts [String] :idempotency_key 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.
109
+ # @return [Array<(CommitAbsenceResolve200Response, Integer, Hash)>] CommitAbsenceResolve200Response data, response status code and response headers
110
+ def commit_absence_resolve_with_http_info(job_request_absence_commit_request, opts = {})
105
111
  if @api_client.config.debugging
106
- @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.get_job_request ...'
112
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.commit_absence_resolve ...'
107
113
  end
108
- # verify the required parameter 'id' is set
109
- if @api_client.config.client_side_validation && id.nil?
110
- fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.get_job_request"
114
+ # verify the required parameter 'job_request_absence_commit_request' is set
115
+ if @api_client.config.client_side_validation && job_request_absence_commit_request.nil?
116
+ fail ArgumentError, "Missing the required parameter 'job_request_absence_commit_request' when calling JobRequestBusinessApi.commit_absence_resolve"
111
117
  end
112
118
  # resource path
113
- local_var_path = '/job-requests/{id}'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
119
+ local_var_path = '/job-requests/absence/commit'
114
120
 
115
121
  # query parameters
116
122
  query_params = opts[:query_params] || {}
@@ -119,21 +125,27 @@ module Crisphive
119
125
  header_params = opts[:header_params] || {}
120
126
  # HTTP header 'Accept' (if needed)
121
127
  header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
128
+ # HTTP header 'Content-Type'
129
+ content_type = @api_client.select_header_content_type(['application/json'])
130
+ if !content_type.nil?
131
+ header_params['Content-Type'] = content_type
132
+ end
133
+ header_params[:'Idempotency-Key'] = opts[:'idempotency_key'] if !opts[:'idempotency_key'].nil?
122
134
 
123
135
  # form parameters
124
136
  form_params = opts[:form_params] || {}
125
137
 
126
138
  # http body (model)
127
- post_body = opts[:debug_body]
139
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_absence_commit_request)
128
140
 
129
141
  # return_type
130
- return_type = opts[:debug_return_type] || 'GetJobRequest200Response'
142
+ return_type = opts[:debug_return_type] || 'CommitAbsenceResolve200Response'
131
143
 
132
144
  # auth_names
133
145
  auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
134
146
 
135
147
  new_options = opts.merge(
136
- :operation => :"JobRequestBusinessApi.get_job_request",
148
+ :operation => :"JobRequestBusinessApi.commit_absence_resolve",
137
149
  :header_params => header_params,
138
150
  :query_params => query_params,
139
151
  :form_params => form_params,
@@ -142,38 +154,117 @@ module Crisphive
142
154
  :return_type => return_type
143
155
  )
144
156
 
145
- data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
157
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
146
158
  if @api_client.config.debugging
147
- @api_client.config.logger.debug "API called: JobRequestBusinessApi#get_job_request\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
159
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#commit_absence_resolve\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
160
+ end
161
+ return data, status_code, headers
162
+ end
163
+
164
+ # Commit emergency insert + cascade reschedule
165
+ # Applies the cascade previewed by /emergency/preview: assigns the emergency job to the technician and pushes the displaced jobs back (or, with `displacement_mode=reassign`, re-staffs them onto their previewed alternates first), atomically. Supports Idempotency-Key. The server recomputes the plan under a lock and fences each job on its status_version — if anything changed since the preview it returns 409 EMERGENCY_RESCHEDULE_PLAN_DRIFTED (re-preview). Same body as preview + optional `emergency_expected_version`. `after_hours_override=true` must match the preview it followed — it drops the non-working-day rejection (this endpoint never runs the technician's working-hours/time-off feasibility check; that only happens on /candidates), and the response carries an AFTER_HOURS warning; the committed job's activity feed also records who authorized the after-hours placement. Isolated feature (see EMERGENCY_RESCHEDULE_DESIGN.md). 409 NEXT STEPS: EMERGENCY_RESCHEDULE_PLAN_DRIFTED — the schedule changed between your preview and this commit (another booking/move won a lane); `data.drifted[]` names the job(s) whose status_version moved when the fence can attribute it (absent, never empty, when only a length mismatch is known): call /preview again, show the fresh plan, then commit. EMERGENCY_RESCHEDULE_SLOT_OCCUPIED — landing window blocked by an immovable anchor (P0/crew/multi-day); `data.conflicts[]` names it: another tech or time. Other codes — same remedies and `data` shapes as /candidates. EMERGENCY_RESCHEDULE_NO_WORKING_DAY fires on a closed day without after_hours_override and carries `data.override_available: true` — confirm with the coordinator, then retry with the flag.
166
+ # @param job_request_emergency_commit_request [JobRequestEmergencyCommitRequest] emergency insert spec
167
+ # @param [Hash] opts the optional parameters
168
+ # @option opts [String] :idempotency_key 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.
169
+ # @return [CommitEmergencyReschedule200Response]
170
+ def commit_emergency_reschedule(job_request_emergency_commit_request, opts = {})
171
+ data, _status_code, _headers = commit_emergency_reschedule_with_http_info(job_request_emergency_commit_request, opts)
172
+ data
173
+ end
174
+
175
+ # Commit emergency insert + cascade reschedule
176
+ # Applies the cascade previewed by /emergency/preview: assigns the emergency job to the technician and pushes the displaced jobs back (or, with &#x60;displacement_mode&#x3D;reassign&#x60;, re-staffs them onto their previewed alternates first), atomically. Supports Idempotency-Key. The server recomputes the plan under a lock and fences each job on its status_version — if anything changed since the preview it returns 409 EMERGENCY_RESCHEDULE_PLAN_DRIFTED (re-preview). Same body as preview + optional &#x60;emergency_expected_version&#x60;. &#x60;after_hours_override&#x3D;true&#x60; must match the preview it followed — it drops the non-working-day rejection (this endpoint never runs the technician&#39;s working-hours/time-off feasibility check; that only happens on /candidates), and the response carries an AFTER_HOURS warning; the committed job&#39;s activity feed also records who authorized the after-hours placement. Isolated feature (see EMERGENCY_RESCHEDULE_DESIGN.md). 409 NEXT STEPS: EMERGENCY_RESCHEDULE_PLAN_DRIFTED — the schedule changed between your preview and this commit (another booking/move won a lane); &#x60;data.drifted[]&#x60; names the job(s) whose status_version moved when the fence can attribute it (absent, never empty, when only a length mismatch is known): call /preview again, show the fresh plan, then commit. EMERGENCY_RESCHEDULE_SLOT_OCCUPIED — landing window blocked by an immovable anchor (P0/crew/multi-day); &#x60;data.conflicts[]&#x60; names it: another tech or time. Other codes — same remedies and &#x60;data&#x60; shapes as /candidates. EMERGENCY_RESCHEDULE_NO_WORKING_DAY fires on a closed day without after_hours_override and carries &#x60;data.override_available: true&#x60; — confirm with the coordinator, then retry with the flag.
177
+ # @param job_request_emergency_commit_request [JobRequestEmergencyCommitRequest] emergency insert spec
178
+ # @param [Hash] opts the optional parameters
179
+ # @option opts [String] :idempotency_key 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.
180
+ # @return [Array<(CommitEmergencyReschedule200Response, Integer, Hash)>] CommitEmergencyReschedule200Response data, response status code and response headers
181
+ def commit_emergency_reschedule_with_http_info(job_request_emergency_commit_request, opts = {})
182
+ if @api_client.config.debugging
183
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.commit_emergency_reschedule ...'
184
+ end
185
+ # verify the required parameter 'job_request_emergency_commit_request' is set
186
+ if @api_client.config.client_side_validation && job_request_emergency_commit_request.nil?
187
+ fail ArgumentError, "Missing the required parameter 'job_request_emergency_commit_request' when calling JobRequestBusinessApi.commit_emergency_reschedule"
188
+ end
189
+ # resource path
190
+ local_var_path = '/job-requests/emergency/commit'
191
+
192
+ # query parameters
193
+ query_params = opts[:query_params] || {}
194
+
195
+ # header parameters
196
+ header_params = opts[:header_params] || {}
197
+ # HTTP header 'Accept' (if needed)
198
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
199
+ # HTTP header 'Content-Type'
200
+ content_type = @api_client.select_header_content_type(['application/json'])
201
+ if !content_type.nil?
202
+ header_params['Content-Type'] = content_type
203
+ end
204
+ header_params[:'Idempotency-Key'] = opts[:'idempotency_key'] if !opts[:'idempotency_key'].nil?
205
+
206
+ # form parameters
207
+ form_params = opts[:form_params] || {}
208
+
209
+ # http body (model)
210
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_emergency_commit_request)
211
+
212
+ # return_type
213
+ return_type = opts[:debug_return_type] || 'CommitEmergencyReschedule200Response'
214
+
215
+ # auth_names
216
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
217
+
218
+ new_options = opts.merge(
219
+ :operation => :"JobRequestBusinessApi.commit_emergency_reschedule",
220
+ :header_params => header_params,
221
+ :query_params => query_params,
222
+ :form_params => form_params,
223
+ :body => post_body,
224
+ :auth_names => auth_names,
225
+ :return_type => return_type
226
+ )
227
+
228
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
229
+ if @api_client.config.debugging
230
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#commit_emergency_reschedule\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
148
231
  end
149
232
  return data, status_code, headers
150
233
  end
151
234
 
152
- # Job timeline (business surface — also serves tech via BusinessAuth)
153
- # Per-status events[] composed from workflow snapshot + scattered typed cols + action_audit. FE renders as the Job Timeline panel (completed step = filled check, current = outline ring, upcoming = empty). entered_at nil for upcoming steps + older jobs missing the typed-col backfill.
235
+ # Commit a schedule-board job move
236
+ # Applies the move previewed by /move/preview: places the job on the technician at the new time and pushes the displaced jobs back, atomically (per-tech advisory lock; the server recomputes the plan and fences each job on its status_version — drift since the preview returns 409 SCHEDULE_MOVE_PLAN_DRIFTED, re-preview). Same body as preview + optional `expected_version`. See SCHEDULE_BOARD_DESIGN.md. 409 NEXT STEPS: SCHEDULE_MOVE_PLAN_DRIFTED — the schedule changed since your preview (or expected_move_ids/expected_member_ids no longer match): re-preview, show the fresh plan, commit again; carries no further `data` (the fence cannot attribute the drift to one specific job). All other codes — same remedies AND `data` shapes as /move/preview.
154
237
  # @param id [String] Job request ID (UUID or short_code)
238
+ # @param job_request_move_commit_req [JobRequestMoveCommitReq] move spec
155
239
  # @param [Hash] opts the optional parameters
156
- # @return [GetJobRequestTimeline200Response]
157
- def get_job_request_timeline(id, opts = {})
158
- data, _status_code, _headers = get_job_request_timeline_with_http_info(id, opts)
240
+ # @option opts [String] :idempotency_key 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.
241
+ # @return [CommitJobRequestMove200Response]
242
+ def commit_job_request_move(id, job_request_move_commit_req, opts = {})
243
+ data, _status_code, _headers = commit_job_request_move_with_http_info(id, job_request_move_commit_req, opts)
159
244
  data
160
245
  end
161
246
 
162
- # Job timeline (business surface — also serves tech via BusinessAuth)
163
- # Per-status events[] composed from workflow snapshot + scattered typed cols + action_audit. FE renders as the Job Timeline panel (completed step &#x3D; filled check, current &#x3D; outline ring, upcoming &#x3D; empty). entered_at nil for upcoming steps + older jobs missing the typed-col backfill.
247
+ # Commit a schedule-board job move
248
+ # Applies the move previewed by /move/preview: places the job on the technician at the new time and pushes the displaced jobs back, atomically (per-tech advisory lock; the server recomputes the plan and fences each job on its status_version — drift since the preview returns 409 SCHEDULE_MOVE_PLAN_DRIFTED, re-preview). Same body as preview + optional &#x60;expected_version&#x60;. See SCHEDULE_BOARD_DESIGN.md. 409 NEXT STEPS: SCHEDULE_MOVE_PLAN_DRIFTED — the schedule changed since your preview (or expected_move_ids/expected_member_ids no longer match): re-preview, show the fresh plan, commit again; carries no further &#x60;data&#x60; (the fence cannot attribute the drift to one specific job). All other codes — same remedies AND &#x60;data&#x60; shapes as /move/preview.
164
249
  # @param id [String] Job request ID (UUID or short_code)
250
+ # @param job_request_move_commit_req [JobRequestMoveCommitReq] move spec
165
251
  # @param [Hash] opts the optional parameters
166
- # @return [Array<(GetJobRequestTimeline200Response, Integer, Hash)>] GetJobRequestTimeline200Response data, response status code and response headers
167
- def get_job_request_timeline_with_http_info(id, opts = {})
252
+ # @option opts [String] :idempotency_key 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.
253
+ # @return [Array<(CommitJobRequestMove200Response, Integer, Hash)>] CommitJobRequestMove200Response data, response status code and response headers
254
+ def commit_job_request_move_with_http_info(id, job_request_move_commit_req, opts = {})
168
255
  if @api_client.config.debugging
169
- @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.get_job_request_timeline ...'
256
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.commit_job_request_move ...'
170
257
  end
171
258
  # verify the required parameter 'id' is set
172
259
  if @api_client.config.client_side_validation && id.nil?
173
- fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.get_job_request_timeline"
260
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.commit_job_request_move"
261
+ end
262
+ # verify the required parameter 'job_request_move_commit_req' is set
263
+ if @api_client.config.client_side_validation && job_request_move_commit_req.nil?
264
+ fail ArgumentError, "Missing the required parameter 'job_request_move_commit_req' when calling JobRequestBusinessApi.commit_job_request_move"
174
265
  end
175
266
  # resource path
176
- local_var_path = '/job-requests/{id}/timeline'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
267
+ local_var_path = '/job-requests/{id}/move/commit'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
177
268
 
178
269
  # query parameters
179
270
  query_params = opts[:query_params] || {}
@@ -182,21 +273,27 @@ module Crisphive
182
273
  header_params = opts[:header_params] || {}
183
274
  # HTTP header 'Accept' (if needed)
184
275
  header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
276
+ # HTTP header 'Content-Type'
277
+ content_type = @api_client.select_header_content_type(['application/json'])
278
+ if !content_type.nil?
279
+ header_params['Content-Type'] = content_type
280
+ end
281
+ header_params[:'Idempotency-Key'] = opts[:'idempotency_key'] if !opts[:'idempotency_key'].nil?
185
282
 
186
283
  # form parameters
187
284
  form_params = opts[:form_params] || {}
188
285
 
189
286
  # http body (model)
190
- post_body = opts[:debug_body]
287
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_move_commit_req)
191
288
 
192
289
  # return_type
193
- return_type = opts[:debug_return_type] || 'GetJobRequestTimeline200Response'
290
+ return_type = opts[:debug_return_type] || 'CommitJobRequestMove200Response'
194
291
 
195
292
  # auth_names
196
293
  auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
197
294
 
198
295
  new_options = opts.merge(
199
- :operation => :"JobRequestBusinessApi.get_job_request_timeline",
296
+ :operation => :"JobRequestBusinessApi.commit_job_request_move",
200
297
  :header_params => header_params,
201
298
  :query_params => query_params,
202
299
  :form_params => form_params,
@@ -205,66 +302,75 @@ module Crisphive
205
302
  :return_type => return_type
206
303
  )
207
304
 
208
- data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
305
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
209
306
  if @api_client.config.debugging
210
- @api_client.config.logger.debug "API called: JobRequestBusinessApi#get_job_request_timeline\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
307
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#commit_job_request_move\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
211
308
  end
212
309
  return data, status_code, headers
213
310
  end
214
311
 
215
- # Booking availability (business actor)
216
- # @param x_timezone [String] Customer IANA timezone
312
+ # Confirm a booking on behalf of the customer
313
+ # 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.
314
+ # @param id [String] Job request ID
315
+ # @param job_request_confirm_request [JobRequestConfirmRequest] Chosen slot (scheduled_at) + optional technician_id force-assign (P0–P3 flow: pins the ranked candidate, feasibility still enforced)
217
316
  # @param [Hash] opts the optional parameters
218
- # @option opts [String] :from Start YYYY-MM-DD
219
- # @option opts [String] :to End YYYY-MM-DD
220
- # @return [ListJobRequestBookingWindows200Response]
221
- def list_job_request_booking_windows(x_timezone, opts = {})
222
- data, _status_code, _headers = list_job_request_booking_windows_with_http_info(x_timezone, opts)
317
+ # @option opts [String] :idempotency_key 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.
318
+ # @return [ResponseEnvelope]
319
+ def confirm_job_request(id, job_request_confirm_request, opts = {})
320
+ data, _status_code, _headers = confirm_job_request_with_http_info(id, job_request_confirm_request, opts)
223
321
  data
224
322
  end
225
323
 
226
- # Booking availability (business actor)
227
- # @param x_timezone [String] Customer IANA timezone
324
+ # Confirm a booking on behalf of the customer
325
+ # Fires the customer-actor &#x60;confirm_booking&#x60; 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&#39;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 &#x60;data&#x60; breakdown: &#x60;considered&#x60; (roster size checked), &#x60;blocked_by&#x60; (histogram of blockers[0].kind → count, only kinds that actually blocked someone), &#x60;truncated&#x60; (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; &#x60;data.reason&#x60; 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 — &#x60;data.earliest_feasible_at&#x60; is when they are free) | cannot_arrive_in_time (see &#x60;data.earliest_feasible_at&#x60;, RFC3339 UTC — the first same-day time they CAN be on site → offer it) | not_available_today (diagnosis unavailable). &#x60;data.blockers[]&#x60; names EVERY cause, most-structural first; &#x60;data.technician&#x60; carries &#x60;{id, name}&#x60;, name best-effort (empty only when the lookup itself could not resolve) — EXCEPT on not_lead_tier, which carries a flat &#x60;data.technician_id&#x60; and no technician object. A UI that reads only &#x60;reason&#x60; 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.
326
+ # @param id [String] Job request ID
327
+ # @param job_request_confirm_request [JobRequestConfirmRequest] Chosen slot (scheduled_at) + optional technician_id force-assign (P0–P3 flow: pins the ranked candidate, feasibility still enforced)
228
328
  # @param [Hash] opts the optional parameters
229
- # @option opts [String] :from Start YYYY-MM-DD
230
- # @option opts [String] :to End YYYY-MM-DD
231
- # @return [Array<(ListJobRequestBookingWindows200Response, Integer, Hash)>] ListJobRequestBookingWindows200Response data, response status code and response headers
232
- def list_job_request_booking_windows_with_http_info(x_timezone, opts = {})
329
+ # @option opts [String] :idempotency_key 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.
330
+ # @return [Array<(ResponseEnvelope, Integer, Hash)>] ResponseEnvelope data, response status code and response headers
331
+ def confirm_job_request_with_http_info(id, job_request_confirm_request, opts = {})
233
332
  if @api_client.config.debugging
234
- @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_job_request_booking_windows ...'
333
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.confirm_job_request ...'
235
334
  end
236
- # verify the required parameter 'x_timezone' is set
237
- if @api_client.config.client_side_validation && x_timezone.nil?
238
- fail ArgumentError, "Missing the required parameter 'x_timezone' when calling JobRequestBusinessApi.list_job_request_booking_windows"
335
+ # verify the required parameter 'id' is set
336
+ if @api_client.config.client_side_validation && id.nil?
337
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.confirm_job_request"
338
+ end
339
+ # verify the required parameter 'job_request_confirm_request' is set
340
+ if @api_client.config.client_side_validation && job_request_confirm_request.nil?
341
+ fail ArgumentError, "Missing the required parameter 'job_request_confirm_request' when calling JobRequestBusinessApi.confirm_job_request"
239
342
  end
240
343
  # resource path
241
- local_var_path = '/job-requests/booking-windows'
344
+ local_var_path = '/job-requests/{id}/confirm'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
242
345
 
243
346
  # query parameters
244
347
  query_params = opts[:query_params] || {}
245
- query_params[:'from'] = opts[:'from'] if !opts[:'from'].nil?
246
- query_params[:'to'] = opts[:'to'] if !opts[:'to'].nil?
247
348
 
248
349
  # header parameters
249
350
  header_params = opts[:header_params] || {}
250
351
  # HTTP header 'Accept' (if needed)
251
352
  header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
252
- header_params[:'X-Timezone'] = x_timezone
353
+ # HTTP header 'Content-Type'
354
+ content_type = @api_client.select_header_content_type(['application/json'])
355
+ if !content_type.nil?
356
+ header_params['Content-Type'] = content_type
357
+ end
358
+ header_params[:'Idempotency-Key'] = opts[:'idempotency_key'] if !opts[:'idempotency_key'].nil?
253
359
 
254
360
  # form parameters
255
361
  form_params = opts[:form_params] || {}
256
362
 
257
363
  # http body (model)
258
- post_body = opts[:debug_body]
364
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_confirm_request)
259
365
 
260
366
  # return_type
261
- return_type = opts[:debug_return_type] || 'ListJobRequestBookingWindows200Response'
367
+ return_type = opts[:debug_return_type] || 'ResponseEnvelope'
262
368
 
263
369
  # auth_names
264
370
  auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
265
371
 
266
372
  new_options = opts.merge(
267
- :operation => :"JobRequestBusinessApi.list_job_request_booking_windows",
373
+ :operation => :"JobRequestBusinessApi.confirm_job_request",
268
374
  :header_params => header_params,
269
375
  :query_params => query_params,
270
376
  :form_params => form_params,
@@ -273,79 +379,72 @@ module Crisphive
273
379
  :return_type => return_type
274
380
  )
275
381
 
276
- data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
382
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
277
383
  if @api_client.config.debugging
278
- @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_job_request_booking_windows\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
384
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#confirm_job_request\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
279
385
  end
280
386
  return data, status_code, headers
281
387
  end
282
388
 
283
- # Poll for new & changed job requests (sync feed)
284
- # Keep your own copy of bookings in sync WITHOUT re-listing everything: returns the job requests whose state changed (created, status transition, reschedule, soft-delete/archive) at or after the `since` cursor, ordered oldest-change-first (updated_at ASC). How to use it: (1) On your first poll OMIT `since` — the server primes the cursor at \"now\", returns no items and a `next_since`. (2) Store `next_since` and pass it as `since` on the next poll. (3) Apply each returned item to your store by UPSERTING on `id` (the server re-scans a ~5s safety window, so the same job may appear again — never blindly append). (4) If `has_more` is true the page filled to `limit` and more changes are already waiting — poll again immediately; otherwise wait your normal interval (e.g. 5–15s). This is NOT pagination — it is a time-keyed change feed. Use the paginated GET /job-requests for the initial bulk load, then this endpoint to stay live. Filters (status_keys, customer_id, …) narrow the feed to the slice you care about.
389
+ # Create a job request
390
+ # Books a field-operations job — the work order that enters the dispatch & scheduling pipeline. Send the customer's UUID plus requested `job_dates` (date + morning/afternoon/evening periods, ideally offered from GET /job-requests/booking-windows), optional `job_type_id` (service catalog), `skill_ids` (required technician qualifications) and a free-text description. Quoting, technician/crew assignment and completion then advance the work order through the business's workflow.
391
+ # @param job_request_create_request [JobRequestCreateRequest] Booking payload
285
392
  # @param [Hash] opts the optional parameters
286
- # @option opts [String] :status_keys Comma-separated status slugs — only surface changes to jobs in these statuses
287
- # @option opts [String] :priority Priority filter (normal|emergency)
288
- # @option opts [String] :customer_id Only changes to this customer&#39;s jobs (UUID)
289
- # @option opts [String] :technician_id Only changes to jobs assigned to this technician (UUID)
290
- # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD or RFC3339); range is [from, to)
291
- # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD or RFC3339), exclusive
292
- # @option opts [String] :since RFC3339 cursor from the prior response&#39;s next_since. OMIT on the first poll to prime the cursor at server-now.
293
- # @option opts [Integer] :limit Max changes per poll (default 15, max 1000). If the page fills, has_more&#x3D;true.
294
- # @return [ListJobRequestChanges200Response]
295
- def list_job_request_changes(opts = {})
296
- data, _status_code, _headers = list_job_request_changes_with_http_info(opts)
393
+ # @option opts [String] :x_timezone Customer IANA timezone
394
+ # @option opts [String] :idempotency_key Unique key making retries safe: a repeat send with the same key returns the original booking instead of creating a duplicate
395
+ # @return [CreateJobRequest200Response]
396
+ def create_job_request(job_request_create_request, opts = {})
397
+ data, _status_code, _headers = create_job_request_with_http_info(job_request_create_request, opts)
297
398
  data
298
399
  end
299
400
 
300
- # Poll for new &amp; changed job requests (sync feed)
301
- # Keep your own copy of bookings in sync WITHOUT re-listing everything: returns the job requests whose state changed (created, status transition, reschedule, soft-delete/archive) at or after the &#x60;since&#x60; cursor, ordered oldest-change-first (updated_at ASC). How to use it: (1) On your first poll OMIT &#x60;since&#x60; — the server primes the cursor at \&quot;now\&quot;, returns no items and a &#x60;next_since&#x60;. (2) Store &#x60;next_since&#x60; and pass it as &#x60;since&#x60; on the next poll. (3) Apply each returned item to your store by UPSERTING on &#x60;id&#x60; (the server re-scans a ~5s safety window, so the same job may appear again — never blindly append). (4) If &#x60;has_more&#x60; is true the page filled to &#x60;limit&#x60; and more changes are already waiting — poll again immediately; otherwise wait your normal interval (e.g. 5–15s). This is NOT pagination — it is a time-keyed change feed. Use the paginated GET /job-requests for the initial bulk load, then this endpoint to stay live. Filters (status_keys, customer_id, …) narrow the feed to the slice you care about.
401
+ # Create a job request
402
+ # Books a field-operations job — the work order that enters the dispatch &amp; scheduling pipeline. Send the customer&#39;s UUID plus requested &#x60;job_dates&#x60; (date + morning/afternoon/evening periods, ideally offered from GET /job-requests/booking-windows), optional &#x60;job_type_id&#x60; (service catalog), &#x60;skill_ids&#x60; (required technician qualifications) and a free-text description. Quoting, technician/crew assignment and completion then advance the work order through the business&#39;s workflow.
403
+ # @param job_request_create_request [JobRequestCreateRequest] Booking payload
302
404
  # @param [Hash] opts the optional parameters
303
- # @option opts [String] :status_keys Comma-separated status slugs — only surface changes to jobs in these statuses
304
- # @option opts [String] :priority Priority filter (normal|emergency)
305
- # @option opts [String] :customer_id Only changes to this customer&#39;s jobs (UUID)
306
- # @option opts [String] :technician_id Only changes to jobs assigned to this technician (UUID)
307
- # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD or RFC3339); range is [from, to)
308
- # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD or RFC3339), exclusive
309
- # @option opts [String] :since RFC3339 cursor from the prior response&#39;s next_since. OMIT on the first poll to prime the cursor at server-now.
310
- # @option opts [Integer] :limit Max changes per poll (default 15, max 1000). If the page fills, has_more&#x3D;true.
311
- # @return [Array<(ListJobRequestChanges200Response, Integer, Hash)>] ListJobRequestChanges200Response data, response status code and response headers
312
- def list_job_request_changes_with_http_info(opts = {})
405
+ # @option opts [String] :x_timezone Customer IANA timezone
406
+ # @option opts [String] :idempotency_key Unique key making retries safe: a repeat send with the same key returns the original booking instead of creating a duplicate
407
+ # @return [Array<(CreateJobRequest200Response, Integer, Hash)>] CreateJobRequest200Response data, response status code and response headers
408
+ def create_job_request_with_http_info(job_request_create_request, opts = {})
313
409
  if @api_client.config.debugging
314
- @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_job_request_changes ...'
410
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.create_job_request ...'
411
+ end
412
+ # verify the required parameter 'job_request_create_request' is set
413
+ if @api_client.config.client_side_validation && job_request_create_request.nil?
414
+ fail ArgumentError, "Missing the required parameter 'job_request_create_request' when calling JobRequestBusinessApi.create_job_request"
315
415
  end
316
416
  # resource path
317
- local_var_path = '/job-requests/changes'
417
+ local_var_path = '/job-requests'
318
418
 
319
419
  # query parameters
320
420
  query_params = opts[:query_params] || {}
321
- query_params[:'status_keys'] = opts[:'status_keys'] if !opts[:'status_keys'].nil?
322
- query_params[:'priority'] = opts[:'priority'] if !opts[:'priority'].nil?
323
- query_params[:'customer_id'] = opts[:'customer_id'] if !opts[:'customer_id'].nil?
324
- query_params[:'technician_id'] = opts[:'technician_id'] if !opts[:'technician_id'].nil?
325
- query_params[:'scheduled_from'] = opts[:'scheduled_from'] if !opts[:'scheduled_from'].nil?
326
- query_params[:'scheduled_to'] = opts[:'scheduled_to'] if !opts[:'scheduled_to'].nil?
327
- query_params[:'since'] = opts[:'since'] if !opts[:'since'].nil?
328
- query_params[:'limit'] = opts[:'limit'] if !opts[:'limit'].nil?
329
421
 
330
422
  # header parameters
331
423
  header_params = opts[:header_params] || {}
332
424
  # HTTP header 'Accept' (if needed)
333
425
  header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
426
+ # HTTP header 'Content-Type'
427
+ content_type = @api_client.select_header_content_type(['application/json'])
428
+ if !content_type.nil?
429
+ header_params['Content-Type'] = content_type
430
+ end
431
+ header_params[:'X-Timezone'] = opts[:'x_timezone'] if !opts[:'x_timezone'].nil?
432
+ header_params[:'Idempotency-Key'] = opts[:'idempotency_key'] if !opts[:'idempotency_key'].nil?
334
433
 
335
434
  # form parameters
336
435
  form_params = opts[:form_params] || {}
337
436
 
338
437
  # http body (model)
339
- post_body = opts[:debug_body]
438
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_create_request)
340
439
 
341
440
  # return_type
342
- return_type = opts[:debug_return_type] || 'ListJobRequestChanges200Response'
441
+ return_type = opts[:debug_return_type] || 'CreateJobRequest200Response'
343
442
 
344
443
  # auth_names
345
444
  auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
346
445
 
347
446
  new_options = opts.merge(
348
- :operation => :"JobRequestBusinessApi.list_job_request_changes",
447
+ :operation => :"JobRequestBusinessApi.create_job_request",
349
448
  :header_params => header_params,
350
449
  :query_params => query_params,
351
450
  :form_params => form_params,
@@ -354,63 +453,41 @@ module Crisphive
354
453
  :return_type => return_type
355
454
  )
356
455
 
357
- data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
456
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
358
457
  if @api_client.config.debugging
359
- @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_job_request_changes\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
458
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#create_job_request\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
360
459
  end
361
460
  return data, status_code, headers
362
461
  end
363
462
 
364
- # List job requests
463
+ # Get a job request
464
+ # Returns the full work order: current workflow status, quoted duration, confirmed schedule, customer contact snapshot and the assigned technician / crew — everything a dispatcher or an external field-operations system needs to track one job.
465
+ # @param id [String] Job request ID (UUID or short_code)
365
466
  # @param [Hash] opts the optional parameters
366
- # @option opts [String] :status_keys Comma-separated status slugs
367
- # @option opts [String] :status active (default) | archived | all
368
- # @option opts [String] :customer_id Customer UUID
369
- # @option opts [String] :technician_id Technician UUID
370
- # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD or RFC3339); range is [from, to)
371
- # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD or RFC3339), exclusive
372
- # @option opts [String] :q Search short_code or description (case-insensitive, partial match)
373
- # @option opts [String] :sort Sort key
374
- # @option opts [Integer] :page Page number
375
- # @option opts [Integer] :limit Page size
376
- # @return [ListJobRequests200Response]
377
- def list_job_requests(opts = {})
378
- data, _status_code, _headers = list_job_requests_with_http_info(opts)
467
+ # @return [GetJobRequest200Response]
468
+ def get_job_request(id, opts = {})
469
+ data, _status_code, _headers = get_job_request_with_http_info(id, opts)
379
470
  data
380
471
  end
381
472
 
382
- # List job requests
473
+ # Get a job request
474
+ # Returns the full work order: current workflow status, quoted duration, confirmed schedule, customer contact snapshot and the assigned technician / crew — everything a dispatcher or an external field-operations system needs to track one job.
475
+ # @param id [String] Job request ID (UUID or short_code)
383
476
  # @param [Hash] opts the optional parameters
384
- # @option opts [String] :status_keys Comma-separated status slugs
385
- # @option opts [String] :status active (default) | archived | all
386
- # @option opts [String] :customer_id Customer UUID
387
- # @option opts [String] :technician_id Technician UUID
388
- # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD or RFC3339); range is [from, to)
389
- # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD or RFC3339), exclusive
390
- # @option opts [String] :q Search short_code or description (case-insensitive, partial match)
391
- # @option opts [String] :sort Sort key
392
- # @option opts [Integer] :page Page number
393
- # @option opts [Integer] :limit Page size
394
- # @return [Array<(ListJobRequests200Response, Integer, Hash)>] ListJobRequests200Response data, response status code and response headers
395
- def list_job_requests_with_http_info(opts = {})
477
+ # @return [Array<(GetJobRequest200Response, Integer, Hash)>] GetJobRequest200Response data, response status code and response headers
478
+ def get_job_request_with_http_info(id, opts = {})
396
479
  if @api_client.config.debugging
397
- @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_job_requests ...'
480
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.get_job_request ...'
481
+ end
482
+ # verify the required parameter 'id' is set
483
+ if @api_client.config.client_side_validation && id.nil?
484
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.get_job_request"
398
485
  end
399
486
  # resource path
400
- local_var_path = '/job-requests'
487
+ local_var_path = '/job-requests/{id}'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
401
488
 
402
489
  # query parameters
403
490
  query_params = opts[:query_params] || {}
404
- query_params[:'status_keys'] = opts[:'status_keys'] if !opts[:'status_keys'].nil?
405
- query_params[:'status'] = opts[:'status'] if !opts[:'status'].nil?
406
- query_params[:'customer_id'] = opts[:'customer_id'] if !opts[:'customer_id'].nil?
407
- query_params[:'technician_id'] = opts[:'technician_id'] if !opts[:'technician_id'].nil?
408
- query_params[:'scheduled_from'] = opts[:'scheduled_from'] if !opts[:'scheduled_from'].nil?
409
- query_params[:'scheduled_to'] = opts[:'scheduled_to'] if !opts[:'scheduled_to'].nil?
410
- query_params[:'q'] = opts[:'q'] if !opts[:'q'].nil?
411
- query_params[:'sort'] = opts[:'sort'] if !opts[:'sort'].nil?
412
- query_params[:'page'] = opts[:'page'] if !opts[:'page'].nil?
413
- query_params[:'limit'] = opts[:'limit'] if !opts[:'limit'].nil?
414
491
 
415
492
  # header parameters
416
493
  header_params = opts[:header_params] || {}
@@ -424,13 +501,13 @@ module Crisphive
424
501
  post_body = opts[:debug_body]
425
502
 
426
503
  # return_type
427
- return_type = opts[:debug_return_type] || 'ListJobRequests200Response'
504
+ return_type = opts[:debug_return_type] || 'GetJobRequest200Response'
428
505
 
429
506
  # auth_names
430
507
  auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
431
508
 
432
509
  new_options = opts.merge(
433
- :operation => :"JobRequestBusinessApi.list_job_requests",
510
+ :operation => :"JobRequestBusinessApi.get_job_request",
434
511
  :header_params => header_params,
435
512
  :query_params => query_params,
436
513
  :form_params => form_params,
@@ -441,7 +518,1036 @@ module Crisphive
441
518
 
442
519
  data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
443
520
  if @api_client.config.debugging
444
- @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_job_requests\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
521
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#get_job_request\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
522
+ end
523
+ return data, status_code, headers
524
+ end
525
+
526
+ # Job timeline
527
+ # Per-status progress of a job's lifecycle (e.g. booked → confirmed → on the way → arrived → completed, following the business's configured workflow) — render it as a job-tracking timeline. Each status carries its state (completed | current | upcoming), when the job entered it, and the actions fired within it. entered_at may be null for upcoming steps and for older jobs predating the backfill.
528
+ # @param id [String] Job request ID (UUID or short_code)
529
+ # @param [Hash] opts the optional parameters
530
+ # @return [GetJobRequestTimeline200Response]
531
+ def get_job_request_timeline(id, opts = {})
532
+ data, _status_code, _headers = get_job_request_timeline_with_http_info(id, opts)
533
+ data
534
+ end
535
+
536
+ # Job timeline
537
+ # Per-status progress of a job&#39;s lifecycle (e.g. booked → confirmed → on the way → arrived → completed, following the business&#39;s configured workflow) — render it as a job-tracking timeline. Each status carries its state (completed | current | upcoming), when the job entered it, and the actions fired within it. entered_at may be null for upcoming steps and for older jobs predating the backfill.
538
+ # @param id [String] Job request ID (UUID or short_code)
539
+ # @param [Hash] opts the optional parameters
540
+ # @return [Array<(GetJobRequestTimeline200Response, Integer, Hash)>] GetJobRequestTimeline200Response data, response status code and response headers
541
+ def get_job_request_timeline_with_http_info(id, opts = {})
542
+ if @api_client.config.debugging
543
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.get_job_request_timeline ...'
544
+ end
545
+ # verify the required parameter 'id' is set
546
+ if @api_client.config.client_side_validation && id.nil?
547
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.get_job_request_timeline"
548
+ end
549
+ # resource path
550
+ local_var_path = '/job-requests/{id}/timeline'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
551
+
552
+ # query parameters
553
+ query_params = opts[:query_params] || {}
554
+
555
+ # header parameters
556
+ header_params = opts[:header_params] || {}
557
+ # HTTP header 'Accept' (if needed)
558
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
559
+
560
+ # form parameters
561
+ form_params = opts[:form_params] || {}
562
+
563
+ # http body (model)
564
+ post_body = opts[:debug_body]
565
+
566
+ # return_type
567
+ return_type = opts[:debug_return_type] || 'GetJobRequestTimeline200Response'
568
+
569
+ # auth_names
570
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
571
+
572
+ new_options = opts.merge(
573
+ :operation => :"JobRequestBusinessApi.get_job_request_timeline",
574
+ :header_params => header_params,
575
+ :query_params => query_params,
576
+ :form_params => form_params,
577
+ :body => post_body,
578
+ :auth_names => auth_names,
579
+ :return_type => return_type
580
+ )
581
+
582
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
583
+ if @api_client.config.debugging
584
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#get_job_request_timeline\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
585
+ end
586
+ return data, status_code, headers
587
+ end
588
+
589
+ # One technician's real schedule (sessions + time off)
590
+ # The technician's ACTUAL occupancy over a date range: every job session on their lane (solo/lead and crew) plus approved time-off blocks. Weekly recurring working hours come from the technician-availability endpoints — combine both for the full availability picture (\"get crew availability\"). from/to are business-local dates (YYYY-MM-DD, inclusive); omitted = today .. +7 days; range max 31 days.
591
+ # @param id [String] Technician ID
592
+ # @param [Hash] opts the optional parameters
593
+ # @option opts [String] :from Start date (YYYY-MM-DD, business-local; default today)
594
+ # @option opts [String] :to End date (YYYY-MM-DD, inclusive; default from+7d; max range 31 days)
595
+ # @return [GetTechnicianSchedule200Response]
596
+ def get_technician_schedule(id, opts = {})
597
+ data, _status_code, _headers = get_technician_schedule_with_http_info(id, opts)
598
+ data
599
+ end
600
+
601
+ # One technician&#39;s real schedule (sessions + time off)
602
+ # The technician&#39;s ACTUAL occupancy over a date range: every job session on their lane (solo/lead and crew) plus approved time-off blocks. Weekly recurring working hours come from the technician-availability endpoints — combine both for the full availability picture (\&quot;get crew availability\&quot;). from/to are business-local dates (YYYY-MM-DD, inclusive); omitted &#x3D; today .. +7 days; range max 31 days.
603
+ # @param id [String] Technician ID
604
+ # @param [Hash] opts the optional parameters
605
+ # @option opts [String] :from Start date (YYYY-MM-DD, business-local; default today)
606
+ # @option opts [String] :to End date (YYYY-MM-DD, inclusive; default from+7d; max range 31 days)
607
+ # @return [Array<(GetTechnicianSchedule200Response, Integer, Hash)>] GetTechnicianSchedule200Response data, response status code and response headers
608
+ def get_technician_schedule_with_http_info(id, opts = {})
609
+ if @api_client.config.debugging
610
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.get_technician_schedule ...'
611
+ end
612
+ # verify the required parameter 'id' is set
613
+ if @api_client.config.client_side_validation && id.nil?
614
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.get_technician_schedule"
615
+ end
616
+ # resource path
617
+ local_var_path = '/technicians/{id}/schedule'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
618
+
619
+ # query parameters
620
+ query_params = opts[:query_params] || {}
621
+ query_params[:'from'] = opts[:'from'] if !opts[:'from'].nil?
622
+ query_params[:'to'] = opts[:'to'] if !opts[:'to'].nil?
623
+
624
+ # header parameters
625
+ header_params = opts[:header_params] || {}
626
+ # HTTP header 'Accept' (if needed)
627
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
628
+
629
+ # form parameters
630
+ form_params = opts[:form_params] || {}
631
+
632
+ # http body (model)
633
+ post_body = opts[:debug_body]
634
+
635
+ # return_type
636
+ return_type = opts[:debug_return_type] || 'GetTechnicianSchedule200Response'
637
+
638
+ # auth_names
639
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
640
+
641
+ new_options = opts.merge(
642
+ :operation => :"JobRequestBusinessApi.get_technician_schedule",
643
+ :header_params => header_params,
644
+ :query_params => query_params,
645
+ :form_params => form_params,
646
+ :body => post_body,
647
+ :auth_names => auth_names,
648
+ :return_type => return_type
649
+ )
650
+
651
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
652
+ if @api_client.config.debugging
653
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#get_technician_schedule\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
654
+ end
655
+ return data, status_code, headers
656
+ end
657
+
658
+ # Matching crew candidates for a job
659
+ # RE-STAFFING candidates for a CONFIRMED, SCHEDULED job (not yet completed/archived) — any earlier or later stage returns 409 JOB_REQUEST_INVALID_TRANSITION. This is the pool of technicians who could REPLACE the current crew: the currently assigned lead and buddies are deliberately excluded (they are the status quo, not an option), so on a small roster an empty `leads` list is a normal answer, not an error. For pre-booking discovery (\"who could take this job before it is confirmed?\") use listJobRequestBookingWindows / listMatchingSlots / the time-segments grid instead. Candidates are matched and ranked by the smart-assignment engine — skills per crew slot, weekly availability, existing schedule, time off and travel are all checked; each carries a score breakdown (distance, travel, matched skills) plus the exact on-site session plan they would work. NOT a raw roster list (use GET /technicians for that). Returns the ranked feasible LEAD pool by default; pass include_buddies=true to also return per-slot buddy pools, include_vehicle=true to include the available-vehicle list. force_lead_id checks one specific technician: returns only that lead (with their crew combo) if feasible, else 409 JOB_REQUEST_NO_TECHNICIAN_AVAILABLE.
660
+ # @param id [String] Job request ID or short_code
661
+ # @param [Hash] opts the optional parameters
662
+ # @option opts [Boolean] :include_buddies Also return buddy candidate pools
663
+ # @option opts [Boolean] :include_vehicle Also return the available-vehicle list
664
+ # @option opts [String] :force_lead_id Check a specific technician as lead — returns only that lead if feasible, else 409
665
+ # @return [ListCrewCandidates200Response]
666
+ def list_crew_candidates(id, opts = {})
667
+ data, _status_code, _headers = list_crew_candidates_with_http_info(id, opts)
668
+ data
669
+ end
670
+
671
+ # Matching crew candidates for a job
672
+ # RE-STAFFING candidates for a CONFIRMED, SCHEDULED job (not yet completed/archived) — any earlier or later stage returns 409 JOB_REQUEST_INVALID_TRANSITION. This is the pool of technicians who could REPLACE the current crew: the currently assigned lead and buddies are deliberately excluded (they are the status quo, not an option), so on a small roster an empty &#x60;leads&#x60; list is a normal answer, not an error. For pre-booking discovery (\&quot;who could take this job before it is confirmed?\&quot;) use listJobRequestBookingWindows / listMatchingSlots / the time-segments grid instead. Candidates are matched and ranked by the smart-assignment engine — skills per crew slot, weekly availability, existing schedule, time off and travel are all checked; each carries a score breakdown (distance, travel, matched skills) plus the exact on-site session plan they would work. NOT a raw roster list (use GET /technicians for that). Returns the ranked feasible LEAD pool by default; pass include_buddies&#x3D;true to also return per-slot buddy pools, include_vehicle&#x3D;true to include the available-vehicle list. force_lead_id checks one specific technician: returns only that lead (with their crew combo) if feasible, else 409 JOB_REQUEST_NO_TECHNICIAN_AVAILABLE.
673
+ # @param id [String] Job request ID or short_code
674
+ # @param [Hash] opts the optional parameters
675
+ # @option opts [Boolean] :include_buddies Also return buddy candidate pools
676
+ # @option opts [Boolean] :include_vehicle Also return the available-vehicle list
677
+ # @option opts [String] :force_lead_id Check a specific technician as lead — returns only that lead if feasible, else 409
678
+ # @return [Array<(ListCrewCandidates200Response, Integer, Hash)>] ListCrewCandidates200Response data, response status code and response headers
679
+ def list_crew_candidates_with_http_info(id, opts = {})
680
+ if @api_client.config.debugging
681
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_crew_candidates ...'
682
+ end
683
+ # verify the required parameter 'id' is set
684
+ if @api_client.config.client_side_validation && id.nil?
685
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.list_crew_candidates"
686
+ end
687
+ # resource path
688
+ local_var_path = '/job-requests/{id}/crew-candidates'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
689
+
690
+ # query parameters
691
+ query_params = opts[:query_params] || {}
692
+ query_params[:'include_buddies'] = opts[:'include_buddies'] if !opts[:'include_buddies'].nil?
693
+ query_params[:'include_vehicle'] = opts[:'include_vehicle'] if !opts[:'include_vehicle'].nil?
694
+ query_params[:'force_lead_id'] = opts[:'force_lead_id'] if !opts[:'force_lead_id'].nil?
695
+
696
+ # header parameters
697
+ header_params = opts[:header_params] || {}
698
+ # HTTP header 'Accept' (if needed)
699
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
700
+
701
+ # form parameters
702
+ form_params = opts[:form_params] || {}
703
+
704
+ # http body (model)
705
+ post_body = opts[:debug_body]
706
+
707
+ # return_type
708
+ return_type = opts[:debug_return_type] || 'ListCrewCandidates200Response'
709
+
710
+ # auth_names
711
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
712
+
713
+ new_options = opts.merge(
714
+ :operation => :"JobRequestBusinessApi.list_crew_candidates",
715
+ :header_params => header_params,
716
+ :query_params => query_params,
717
+ :form_params => form_params,
718
+ :body => post_body,
719
+ :auth_names => auth_names,
720
+ :return_type => return_type
721
+ )
722
+
723
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
724
+ if @api_client.config.debugging
725
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_crew_candidates\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
726
+ end
727
+ return data, status_code, headers
728
+ end
729
+
730
+ # Rank technicians for a P0 emergency insert
731
+ # Returns the technicians who could take the emergency job at the requested start, ranked FASTEST-ARRIVAL first (arrival beats route efficiency for a P0). The response also carries a historical `crew_recommendation` (median crew size on comparable completed jobs + mandatory disclaimer — AC-2). Booked technicians are still candidates — each entry carries the displacement preview (which lower-priority jobs would be pushed, per day) that committing to them would cause; total_moves=0 means a free slot. P0 jobs are never displaced; P1 only by a P0. ETA is estimated from the technician's start location (no live GPS). `after_hours_override=true` — the coordinator has phoned the technician — drops the non-working-day rejection AND each candidate's working-hours/time-off feasibility check; an affected candidate carries a per-technician TIME_OFF_OVERLAP warning instead. This is THE phone list for an after-hours insert: it ranks even on a day with no working hours once the flag is set. Feed the chosen technician_id into emergency/preview + emergency/commit. 409 NEXT STEPS: EMERGENCY_RESCHEDULE_NOT_ELIGIBLE — the job cannot be emergency-inserted; `data.failed_precondition` names which (not_quoted | archived | completed | not_p0 | smart_assign_unavailable): fix the job state or use a normal confirm. EMERGENCY_RESCHEDULE_CREW_UNSUPPORTED — crew jobs cannot use the emergency flow (v1, `data.crew_size` = lead + buddies): staff via confirm/reassign instead. EMERGENCY_RESCHEDULE_MULTIDAY_UNSUPPORTED — either a confirmed multi-day job (`data.session_count`/`data.session_dates`; use the normal reassign flow) or a single visit longer than the structural span bound, in which case `data.reason=visit_too_long` + `data.blockers[0]` (visit_minutes/max_minutes) name it — no remedy but a shorter visit. EMERGENCY_RESCHEDULE_NO_WORKING_DAY — the chosen date has no working hours (`data.business_timezone`/`data.requested_weekday`): pick a working day, or set after_hours_override=true (the coordinator has phoned someone) to rank candidates anyway. EMERGENCY_RESCHEDULE_IN_PAST — start time already passed: pick a future time; `data` carries the timezone the naive start_at was read in (`business_timezone`) plus the instant it resolved to, so a start that looks future on the caller's own clock can be diagnosed without guessing.
732
+ # @param job_request_emergency_candidates_request [JobRequestEmergencyCandidatesRequest] Emergency job + desired start
733
+ # @param [Hash] opts the optional parameters
734
+ # @return [ListEmergencyCandidates200Response]
735
+ def list_emergency_candidates(job_request_emergency_candidates_request, opts = {})
736
+ data, _status_code, _headers = list_emergency_candidates_with_http_info(job_request_emergency_candidates_request, opts)
737
+ data
738
+ end
739
+
740
+ # Rank technicians for a P0 emergency insert
741
+ # Returns the technicians who could take the emergency job at the requested start, ranked FASTEST-ARRIVAL first (arrival beats route efficiency for a P0). The response also carries a historical &#x60;crew_recommendation&#x60; (median crew size on comparable completed jobs + mandatory disclaimer — AC-2). Booked technicians are still candidates — each entry carries the displacement preview (which lower-priority jobs would be pushed, per day) that committing to them would cause; total_moves&#x3D;0 means a free slot. P0 jobs are never displaced; P1 only by a P0. ETA is estimated from the technician&#39;s start location (no live GPS). &#x60;after_hours_override&#x3D;true&#x60; — the coordinator has phoned the technician — drops the non-working-day rejection AND each candidate&#39;s working-hours/time-off feasibility check; an affected candidate carries a per-technician TIME_OFF_OVERLAP warning instead. This is THE phone list for an after-hours insert: it ranks even on a day with no working hours once the flag is set. Feed the chosen technician_id into emergency/preview + emergency/commit. 409 NEXT STEPS: EMERGENCY_RESCHEDULE_NOT_ELIGIBLE — the job cannot be emergency-inserted; &#x60;data.failed_precondition&#x60; names which (not_quoted | archived | completed | not_p0 | smart_assign_unavailable): fix the job state or use a normal confirm. EMERGENCY_RESCHEDULE_CREW_UNSUPPORTED — crew jobs cannot use the emergency flow (v1, &#x60;data.crew_size&#x60; &#x3D; lead + buddies): staff via confirm/reassign instead. EMERGENCY_RESCHEDULE_MULTIDAY_UNSUPPORTED — either a confirmed multi-day job (&#x60;data.session_count&#x60;/&#x60;data.session_dates&#x60;; use the normal reassign flow) or a single visit longer than the structural span bound, in which case &#x60;data.reason&#x3D;visit_too_long&#x60; + &#x60;data.blockers[0]&#x60; (visit_minutes/max_minutes) name it — no remedy but a shorter visit. EMERGENCY_RESCHEDULE_NO_WORKING_DAY — the chosen date has no working hours (&#x60;data.business_timezone&#x60;/&#x60;data.requested_weekday&#x60;): pick a working day, or set after_hours_override&#x3D;true (the coordinator has phoned someone) to rank candidates anyway. EMERGENCY_RESCHEDULE_IN_PAST — start time already passed: pick a future time; &#x60;data&#x60; carries the timezone the naive start_at was read in (&#x60;business_timezone&#x60;) plus the instant it resolved to, so a start that looks future on the caller&#39;s own clock can be diagnosed without guessing.
742
+ # @param job_request_emergency_candidates_request [JobRequestEmergencyCandidatesRequest] Emergency job + desired start
743
+ # @param [Hash] opts the optional parameters
744
+ # @return [Array<(ListEmergencyCandidates200Response, Integer, Hash)>] ListEmergencyCandidates200Response data, response status code and response headers
745
+ def list_emergency_candidates_with_http_info(job_request_emergency_candidates_request, opts = {})
746
+ if @api_client.config.debugging
747
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_emergency_candidates ...'
748
+ end
749
+ # verify the required parameter 'job_request_emergency_candidates_request' is set
750
+ if @api_client.config.client_side_validation && job_request_emergency_candidates_request.nil?
751
+ fail ArgumentError, "Missing the required parameter 'job_request_emergency_candidates_request' when calling JobRequestBusinessApi.list_emergency_candidates"
752
+ end
753
+ # resource path
754
+ local_var_path = '/job-requests/emergency/candidates'
755
+
756
+ # query parameters
757
+ query_params = opts[:query_params] || {}
758
+
759
+ # header parameters
760
+ header_params = opts[:header_params] || {}
761
+ # HTTP header 'Accept' (if needed)
762
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
763
+ # HTTP header 'Content-Type'
764
+ content_type = @api_client.select_header_content_type(['application/json'])
765
+ if !content_type.nil?
766
+ header_params['Content-Type'] = content_type
767
+ end
768
+
769
+ # form parameters
770
+ form_params = opts[:form_params] || {}
771
+
772
+ # http body (model)
773
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_emergency_candidates_request)
774
+
775
+ # return_type
776
+ return_type = opts[:debug_return_type] || 'ListEmergencyCandidates200Response'
777
+
778
+ # auth_names
779
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
780
+
781
+ new_options = opts.merge(
782
+ :operation => :"JobRequestBusinessApi.list_emergency_candidates",
783
+ :header_params => header_params,
784
+ :query_params => query_params,
785
+ :form_params => form_params,
786
+ :body => post_body,
787
+ :auth_names => auth_names,
788
+ :return_type => return_type
789
+ )
790
+
791
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
792
+ if @api_client.config.debugging
793
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_emergency_candidates\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
794
+ end
795
+ return data, status_code, headers
796
+ end
797
+
798
+ # Booking availability
799
+ # Real-time appointment availability from the scheduling engine: returns the bookable date + time-period windows given technician capacity, working hours and service-territory coverage. Call this before creating a job request and offer the customer ONLY the returned windows — it prevents unschedulable bookings.
800
+ # @param x_timezone [String] Customer IANA timezone
801
+ # @param [Hash] opts the optional parameters
802
+ # @option opts [String] :from Start YYYY-MM-DD
803
+ # @option opts [String] :to End YYYY-MM-DD
804
+ # @return [ListJobRequestBookingWindows200Response]
805
+ def list_job_request_booking_windows(x_timezone, opts = {})
806
+ data, _status_code, _headers = list_job_request_booking_windows_with_http_info(x_timezone, opts)
807
+ data
808
+ end
809
+
810
+ # Booking availability
811
+ # Real-time appointment availability from the scheduling engine: returns the bookable date + time-period windows given technician capacity, working hours and service-territory coverage. Call this before creating a job request and offer the customer ONLY the returned windows — it prevents unschedulable bookings.
812
+ # @param x_timezone [String] Customer IANA timezone
813
+ # @param [Hash] opts the optional parameters
814
+ # @option opts [String] :from Start YYYY-MM-DD
815
+ # @option opts [String] :to End YYYY-MM-DD
816
+ # @return [Array<(ListJobRequestBookingWindows200Response, Integer, Hash)>] ListJobRequestBookingWindows200Response data, response status code and response headers
817
+ def list_job_request_booking_windows_with_http_info(x_timezone, opts = {})
818
+ if @api_client.config.debugging
819
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_job_request_booking_windows ...'
820
+ end
821
+ # verify the required parameter 'x_timezone' is set
822
+ if @api_client.config.client_side_validation && x_timezone.nil?
823
+ fail ArgumentError, "Missing the required parameter 'x_timezone' when calling JobRequestBusinessApi.list_job_request_booking_windows"
824
+ end
825
+ # resource path
826
+ local_var_path = '/job-requests/booking-windows'
827
+
828
+ # query parameters
829
+ query_params = opts[:query_params] || {}
830
+ query_params[:'from'] = opts[:'from'] if !opts[:'from'].nil?
831
+ query_params[:'to'] = opts[:'to'] if !opts[:'to'].nil?
832
+
833
+ # header parameters
834
+ header_params = opts[:header_params] || {}
835
+ # HTTP header 'Accept' (if needed)
836
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
837
+ header_params[:'X-Timezone'] = x_timezone
838
+
839
+ # form parameters
840
+ form_params = opts[:form_params] || {}
841
+
842
+ # http body (model)
843
+ post_body = opts[:debug_body]
844
+
845
+ # return_type
846
+ return_type = opts[:debug_return_type] || 'ListJobRequestBookingWindows200Response'
847
+
848
+ # auth_names
849
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
850
+
851
+ new_options = opts.merge(
852
+ :operation => :"JobRequestBusinessApi.list_job_request_booking_windows",
853
+ :header_params => header_params,
854
+ :query_params => query_params,
855
+ :form_params => form_params,
856
+ :body => post_body,
857
+ :auth_names => auth_names,
858
+ :return_type => return_type
859
+ )
860
+
861
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
862
+ if @api_client.config.debugging
863
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_job_request_booking_windows\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
864
+ end
865
+ return data, status_code, headers
866
+ end
867
+
868
+ # Poll for new & changed job requests (sync feed)
869
+ # Keep an external system (your CRM, ERP or field-operations tool) in sync with bookings WITHOUT re-listing everything: returns the job requests (work orders) whose state changed (created, status transition, reschedule, soft-delete/archive) at or after the `since` cursor, ordered oldest-change-first (updated_at ASC). How to use it: (1) On your first poll OMIT `since` — the server primes the cursor at \"now\", returns no items and a `next_since`. (2) Store `next_since` and pass it as `since` on the next poll. (3) Apply each returned item to your store by UPSERTING on `id` (the server re-scans a ~5s safety window, so the same job may appear again — never blindly append). (4) If `has_more` is true the page filled to `limit` and more changes are already waiting — poll again immediately; otherwise wait your normal interval (e.g. 5–15s). This is NOT pagination — it is a time-keyed change feed. Use the paginated GET /job-requests for the initial bulk load, then this endpoint to stay live. Filters (status_keys, customer_id, …) narrow the feed to the slice you care about.
870
+ # @param [Hash] opts the optional parameters
871
+ # @option opts [String] :status_keys Comma-separated status slugs — only surface changes to jobs in these statuses
872
+ # @option opts [String] :priority Priority filter (p0|p1|p2|p3)
873
+ # @option opts [String] :customer_id Only changes to this customer&#39;s jobs (UUID)
874
+ # @option opts [String] :technician_id Only changes to jobs assigned to this technician (UUID)
875
+ # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD &#x3D; start of that day in the business timezone, or RFC3339); range is [from, to)
876
+ # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD &#x3D; end of that day in the business timezone, or RFC3339), exclusive
877
+ # @option opts [String] :since RFC3339 cursor from the prior response&#39;s next_since. OMIT on the first poll to prime the cursor at server-now.
878
+ # @option opts [Integer] :limit Max changes per poll (default 15, max 1000). If the page fills, has_more&#x3D;true.
879
+ # @return [ListJobRequestChanges200Response]
880
+ def list_job_request_changes(opts = {})
881
+ data, _status_code, _headers = list_job_request_changes_with_http_info(opts)
882
+ data
883
+ end
884
+
885
+ # Poll for new &amp; changed job requests (sync feed)
886
+ # Keep an external system (your CRM, ERP or field-operations tool) in sync with bookings WITHOUT re-listing everything: returns the job requests (work orders) whose state changed (created, status transition, reschedule, soft-delete/archive) at or after the &#x60;since&#x60; cursor, ordered oldest-change-first (updated_at ASC). How to use it: (1) On your first poll OMIT &#x60;since&#x60; — the server primes the cursor at \&quot;now\&quot;, returns no items and a &#x60;next_since&#x60;. (2) Store &#x60;next_since&#x60; and pass it as &#x60;since&#x60; on the next poll. (3) Apply each returned item to your store by UPSERTING on &#x60;id&#x60; (the server re-scans a ~5s safety window, so the same job may appear again — never blindly append). (4) If &#x60;has_more&#x60; is true the page filled to &#x60;limit&#x60; and more changes are already waiting — poll again immediately; otherwise wait your normal interval (e.g. 5–15s). This is NOT pagination — it is a time-keyed change feed. Use the paginated GET /job-requests for the initial bulk load, then this endpoint to stay live. Filters (status_keys, customer_id, …) narrow the feed to the slice you care about.
887
+ # @param [Hash] opts the optional parameters
888
+ # @option opts [String] :status_keys Comma-separated status slugs — only surface changes to jobs in these statuses
889
+ # @option opts [String] :priority Priority filter (p0|p1|p2|p3)
890
+ # @option opts [String] :customer_id Only changes to this customer&#39;s jobs (UUID)
891
+ # @option opts [String] :technician_id Only changes to jobs assigned to this technician (UUID)
892
+ # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD &#x3D; start of that day in the business timezone, or RFC3339); range is [from, to)
893
+ # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD &#x3D; end of that day in the business timezone, or RFC3339), exclusive
894
+ # @option opts [String] :since RFC3339 cursor from the prior response&#39;s next_since. OMIT on the first poll to prime the cursor at server-now.
895
+ # @option opts [Integer] :limit Max changes per poll (default 15, max 1000). If the page fills, has_more&#x3D;true.
896
+ # @return [Array<(ListJobRequestChanges200Response, Integer, Hash)>] ListJobRequestChanges200Response data, response status code and response headers
897
+ def list_job_request_changes_with_http_info(opts = {})
898
+ if @api_client.config.debugging
899
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_job_request_changes ...'
900
+ end
901
+ # resource path
902
+ local_var_path = '/job-requests/changes'
903
+
904
+ # query parameters
905
+ query_params = opts[:query_params] || {}
906
+ query_params[:'status_keys'] = opts[:'status_keys'] if !opts[:'status_keys'].nil?
907
+ query_params[:'priority'] = opts[:'priority'] if !opts[:'priority'].nil?
908
+ query_params[:'customer_id'] = opts[:'customer_id'] if !opts[:'customer_id'].nil?
909
+ query_params[:'technician_id'] = opts[:'technician_id'] if !opts[:'technician_id'].nil?
910
+ query_params[:'scheduled_from'] = opts[:'scheduled_from'] if !opts[:'scheduled_from'].nil?
911
+ query_params[:'scheduled_to'] = opts[:'scheduled_to'] if !opts[:'scheduled_to'].nil?
912
+ query_params[:'since'] = opts[:'since'] if !opts[:'since'].nil?
913
+ query_params[:'limit'] = opts[:'limit'] if !opts[:'limit'].nil?
914
+
915
+ # header parameters
916
+ header_params = opts[:header_params] || {}
917
+ # HTTP header 'Accept' (if needed)
918
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
919
+
920
+ # form parameters
921
+ form_params = opts[:form_params] || {}
922
+
923
+ # http body (model)
924
+ post_body = opts[:debug_body]
925
+
926
+ # return_type
927
+ return_type = opts[:debug_return_type] || 'ListJobRequestChanges200Response'
928
+
929
+ # auth_names
930
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
931
+
932
+ new_options = opts.merge(
933
+ :operation => :"JobRequestBusinessApi.list_job_request_changes",
934
+ :header_params => header_params,
935
+ :query_params => query_params,
936
+ :form_params => form_params,
937
+ :body => post_body,
938
+ :auth_names => auth_names,
939
+ :return_type => return_type
940
+ )
941
+
942
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
943
+ if @api_client.config.debugging
944
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_job_request_changes\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
945
+ end
946
+ return data, status_code, headers
947
+ end
948
+
949
+ # List job requests
950
+ # Paginated list of the business's bookings (work orders) with dispatch-oriented filters: workflow status, customer, assigned technician, scheduled date range and free-text search over code/description. This is also the SCHEDULE query: combine technician_id + scheduled_from/scheduled_to to read one technician's agenda for a day or week (e.g. \"what is Alex doing tomorrow\"), or just the date range for the whole team's calendar.
951
+ # @param [Hash] opts the optional parameters
952
+ # @option opts [String] :status_keys Comma-separated status slugs
953
+ # @option opts [String] :status active (default) | archived | all
954
+ # @option opts [String] :priority Priority filter (p0|p1|p2|p3)
955
+ # @option opts [String] :customer_id Customer UUID
956
+ # @option opts [String] :technician_id Technician UUID
957
+ # @option opts [String] :service_area_id Service-area UUID (board zone filter)
958
+ # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD &#x3D; start of that day in the business timezone, or RFC3339); range is [from, to)
959
+ # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD &#x3D; end of that day in the business timezone, or RFC3339), exclusive
960
+ # @option opts [String] :completed_from Filter by completion time from (YYYY-MM-DD &#x3D; start of that day in the business timezone, or RFC3339); range is [from, to). Counts jobs by when they were completed regardless of reschedules.
961
+ # @option opts [String] :completed_to Filter by completion time to (YYYY-MM-DD &#x3D; end of that day in the business timezone, or RFC3339), exclusive
962
+ # @option opts [String] :q Search short_code or description (case-insensitive, partial match)
963
+ # @option opts [String] :sort Sort key: created_at:desc (default) | created_at:asc | scheduled_at:asc | scheduled_at:desc | priority:asc (P0 first) | priority:desc
964
+ # @option opts [Integer] :page Page number
965
+ # @option opts [Integer] :limit Page size
966
+ # @return [ListJobRequests200Response]
967
+ def list_job_requests(opts = {})
968
+ data, _status_code, _headers = list_job_requests_with_http_info(opts)
969
+ data
970
+ end
971
+
972
+ # List job requests
973
+ # Paginated list of the business&#39;s bookings (work orders) with dispatch-oriented filters: workflow status, customer, assigned technician, scheduled date range and free-text search over code/description. This is also the SCHEDULE query: combine technician_id + scheduled_from/scheduled_to to read one technician&#39;s agenda for a day or week (e.g. \&quot;what is Alex doing tomorrow\&quot;), or just the date range for the whole team&#39;s calendar.
974
+ # @param [Hash] opts the optional parameters
975
+ # @option opts [String] :status_keys Comma-separated status slugs
976
+ # @option opts [String] :status active (default) | archived | all
977
+ # @option opts [String] :priority Priority filter (p0|p1|p2|p3)
978
+ # @option opts [String] :customer_id Customer UUID
979
+ # @option opts [String] :technician_id Technician UUID
980
+ # @option opts [String] :service_area_id Service-area UUID (board zone filter)
981
+ # @option opts [String] :scheduled_from Filter from (YYYY-MM-DD &#x3D; start of that day in the business timezone, or RFC3339); range is [from, to)
982
+ # @option opts [String] :scheduled_to Filter to (YYYY-MM-DD &#x3D; end of that day in the business timezone, or RFC3339), exclusive
983
+ # @option opts [String] :completed_from Filter by completion time from (YYYY-MM-DD &#x3D; start of that day in the business timezone, or RFC3339); range is [from, to). Counts jobs by when they were completed regardless of reschedules.
984
+ # @option opts [String] :completed_to Filter by completion time to (YYYY-MM-DD &#x3D; end of that day in the business timezone, or RFC3339), exclusive
985
+ # @option opts [String] :q Search short_code or description (case-insensitive, partial match)
986
+ # @option opts [String] :sort Sort key: created_at:desc (default) | created_at:asc | scheduled_at:asc | scheduled_at:desc | priority:asc (P0 first) | priority:desc
987
+ # @option opts [Integer] :page Page number
988
+ # @option opts [Integer] :limit Page size
989
+ # @return [Array<(ListJobRequests200Response, Integer, Hash)>] ListJobRequests200Response data, response status code and response headers
990
+ def list_job_requests_with_http_info(opts = {})
991
+ if @api_client.config.debugging
992
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_job_requests ...'
993
+ end
994
+ # resource path
995
+ local_var_path = '/job-requests'
996
+
997
+ # query parameters
998
+ query_params = opts[:query_params] || {}
999
+ query_params[:'status_keys'] = opts[:'status_keys'] if !opts[:'status_keys'].nil?
1000
+ query_params[:'status'] = opts[:'status'] if !opts[:'status'].nil?
1001
+ query_params[:'priority'] = opts[:'priority'] if !opts[:'priority'].nil?
1002
+ query_params[:'customer_id'] = opts[:'customer_id'] if !opts[:'customer_id'].nil?
1003
+ query_params[:'technician_id'] = opts[:'technician_id'] if !opts[:'technician_id'].nil?
1004
+ query_params[:'service_area_id'] = opts[:'service_area_id'] if !opts[:'service_area_id'].nil?
1005
+ query_params[:'scheduled_from'] = opts[:'scheduled_from'] if !opts[:'scheduled_from'].nil?
1006
+ query_params[:'scheduled_to'] = opts[:'scheduled_to'] if !opts[:'scheduled_to'].nil?
1007
+ query_params[:'completed_from'] = opts[:'completed_from'] if !opts[:'completed_from'].nil?
1008
+ query_params[:'completed_to'] = opts[:'completed_to'] if !opts[:'completed_to'].nil?
1009
+ query_params[:'q'] = opts[:'q'] if !opts[:'q'].nil?
1010
+ query_params[:'sort'] = opts[:'sort'] if !opts[:'sort'].nil?
1011
+ query_params[:'page'] = opts[:'page'] if !opts[:'page'].nil?
1012
+ query_params[:'limit'] = opts[:'limit'] if !opts[:'limit'].nil?
1013
+
1014
+ # header parameters
1015
+ header_params = opts[:header_params] || {}
1016
+ # HTTP header 'Accept' (if needed)
1017
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1018
+
1019
+ # form parameters
1020
+ form_params = opts[:form_params] || {}
1021
+
1022
+ # http body (model)
1023
+ post_body = opts[:debug_body]
1024
+
1025
+ # return_type
1026
+ return_type = opts[:debug_return_type] || 'ListJobRequests200Response'
1027
+
1028
+ # auth_names
1029
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1030
+
1031
+ new_options = opts.merge(
1032
+ :operation => :"JobRequestBusinessApi.list_job_requests",
1033
+ :header_params => header_params,
1034
+ :query_params => query_params,
1035
+ :form_params => form_params,
1036
+ :body => post_body,
1037
+ :auth_names => auth_names,
1038
+ :return_type => return_type
1039
+ )
1040
+
1041
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
1042
+ if @api_client.config.debugging
1043
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_job_requests\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
1044
+ end
1045
+ return data, status_code, headers
1046
+ end
1047
+
1048
+ # Matching time slots for a quoted job
1049
+ # Bookable arrival-window slots for a quoted job, computed by the smart-assignment matching engine: each slot lists the technicians actually available to start then (skills, weekly availability, existing schedule, time off and travel all checked), with a per-technician match score. Use it to find and offer appointment times an agent or integration can then confirm (POST /job-requests/{id}/confirm with the slot's business_time.datetime). Same grid the end-customer's slot picker shows; slot width defaults to the business's arrival window — override via ?step_minutes (5–240). The job must be quoted first (the quote sets the visit duration the matcher schedules).
1050
+ # @param id [String] Job request ID (UUID or short_code)
1051
+ # @param [Hash] opts the optional parameters
1052
+ # @option opts [Integer] :step_minutes Slot step in minutes (default: business arrival window, 5–240)
1053
+ # @return [ListMatchingSlots200Response]
1054
+ def list_matching_slots(id, opts = {})
1055
+ data, _status_code, _headers = list_matching_slots_with_http_info(id, opts)
1056
+ data
1057
+ end
1058
+
1059
+ # Matching time slots for a quoted job
1060
+ # Bookable arrival-window slots for a quoted job, computed by the smart-assignment matching engine: each slot lists the technicians actually available to start then (skills, weekly availability, existing schedule, time off and travel all checked), with a per-technician match score. Use it to find and offer appointment times an agent or integration can then confirm (POST /job-requests/{id}/confirm with the slot&#39;s business_time.datetime). Same grid the end-customer&#39;s slot picker shows; slot width defaults to the business&#39;s arrival window — override via ?step_minutes (5–240). The job must be quoted first (the quote sets the visit duration the matcher schedules).
1061
+ # @param id [String] Job request ID (UUID or short_code)
1062
+ # @param [Hash] opts the optional parameters
1063
+ # @option opts [Integer] :step_minutes Slot step in minutes (default: business arrival window, 5–240)
1064
+ # @return [Array<(ListMatchingSlots200Response, Integer, Hash)>] ListMatchingSlots200Response data, response status code and response headers
1065
+ def list_matching_slots_with_http_info(id, opts = {})
1066
+ if @api_client.config.debugging
1067
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_matching_slots ...'
1068
+ end
1069
+ # verify the required parameter 'id' is set
1070
+ if @api_client.config.client_side_validation && id.nil?
1071
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.list_matching_slots"
1072
+ end
1073
+ # resource path
1074
+ local_var_path = '/job-requests/{id}/time-segments'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
1075
+
1076
+ # query parameters
1077
+ query_params = opts[:query_params] || {}
1078
+ query_params[:'step_minutes'] = opts[:'step_minutes'] if !opts[:'step_minutes'].nil?
1079
+
1080
+ # header parameters
1081
+ header_params = opts[:header_params] || {}
1082
+ # HTTP header 'Accept' (if needed)
1083
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1084
+
1085
+ # form parameters
1086
+ form_params = opts[:form_params] || {}
1087
+
1088
+ # http body (model)
1089
+ post_body = opts[:debug_body]
1090
+
1091
+ # return_type
1092
+ return_type = opts[:debug_return_type] || 'ListMatchingSlots200Response'
1093
+
1094
+ # auth_names
1095
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1096
+
1097
+ new_options = opts.merge(
1098
+ :operation => :"JobRequestBusinessApi.list_matching_slots",
1099
+ :header_params => header_params,
1100
+ :query_params => query_params,
1101
+ :form_params => form_params,
1102
+ :body => post_body,
1103
+ :auth_names => auth_names,
1104
+ :return_type => return_type
1105
+ )
1106
+
1107
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
1108
+ if @api_client.config.debugging
1109
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_matching_slots\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
1110
+ end
1111
+ return data, status_code, headers
1112
+ end
1113
+
1114
+ # Find nearby feasible technicians (job-less location query)
1115
+ # Ranks who could serve a hypothetical visit at (lat,lng) starting `at` for `duration_minutes` — the engine applies the REAL hard filters (weekly hours, existing schedule, approved time-off, geographic service areas, optional skill floor) and returns candidates nearest-arrival first. ETA origin is each technician's start location (no live GPS). Use before creating a booking to propose realistic arrivals.
1116
+ # @param lat [Float] Latitude of the service location
1117
+ # @param lng [Float] Longitude of the service location
1118
+ # @param [Hash] opts the optional parameters
1119
+ # @option opts [String] :at Visit start (RFC3339, e.g. 2026-07-20T14:00:00Z; default now)
1120
+ # @option opts [Integer] :duration_minutes Visit length in minutes (default 60; 15–480)
1121
+ # @option opts [String] :skill_ids Comma-separated skill UUIDs to require/match
1122
+ # @option opts [Integer] :limit Max candidates (default 10, max 20)
1123
+ # @return [ListNearbyTechnicians200Response]
1124
+ def list_nearby_technicians(lat, lng, opts = {})
1125
+ data, _status_code, _headers = list_nearby_technicians_with_http_info(lat, lng, opts)
1126
+ data
1127
+ end
1128
+
1129
+ # Find nearby feasible technicians (job-less location query)
1130
+ # Ranks who could serve a hypothetical visit at (lat,lng) starting &#x60;at&#x60; for &#x60;duration_minutes&#x60; — the engine applies the REAL hard filters (weekly hours, existing schedule, approved time-off, geographic service areas, optional skill floor) and returns candidates nearest-arrival first. ETA origin is each technician&#39;s start location (no live GPS). Use before creating a booking to propose realistic arrivals.
1131
+ # @param lat [Float] Latitude of the service location
1132
+ # @param lng [Float] Longitude of the service location
1133
+ # @param [Hash] opts the optional parameters
1134
+ # @option opts [String] :at Visit start (RFC3339, e.g. 2026-07-20T14:00:00Z; default now)
1135
+ # @option opts [Integer] :duration_minutes Visit length in minutes (default 60; 15–480)
1136
+ # @option opts [String] :skill_ids Comma-separated skill UUIDs to require/match
1137
+ # @option opts [Integer] :limit Max candidates (default 10, max 20)
1138
+ # @return [Array<(ListNearbyTechnicians200Response, Integer, Hash)>] ListNearbyTechnicians200Response data, response status code and response headers
1139
+ def list_nearby_technicians_with_http_info(lat, lng, opts = {})
1140
+ if @api_client.config.debugging
1141
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.list_nearby_technicians ...'
1142
+ end
1143
+ # verify the required parameter 'lat' is set
1144
+ if @api_client.config.client_side_validation && lat.nil?
1145
+ fail ArgumentError, "Missing the required parameter 'lat' when calling JobRequestBusinessApi.list_nearby_technicians"
1146
+ end
1147
+ # verify the required parameter 'lng' is set
1148
+ if @api_client.config.client_side_validation && lng.nil?
1149
+ fail ArgumentError, "Missing the required parameter 'lng' when calling JobRequestBusinessApi.list_nearby_technicians"
1150
+ end
1151
+ # resource path
1152
+ local_var_path = '/technicians/nearby'
1153
+
1154
+ # query parameters
1155
+ query_params = opts[:query_params] || {}
1156
+ query_params[:'lat'] = lat
1157
+ query_params[:'lng'] = lng
1158
+ query_params[:'at'] = opts[:'at'] if !opts[:'at'].nil?
1159
+ query_params[:'duration_minutes'] = opts[:'duration_minutes'] if !opts[:'duration_minutes'].nil?
1160
+ query_params[:'skill_ids'] = opts[:'skill_ids'] if !opts[:'skill_ids'].nil?
1161
+ query_params[:'limit'] = opts[:'limit'] if !opts[:'limit'].nil?
1162
+
1163
+ # header parameters
1164
+ header_params = opts[:header_params] || {}
1165
+ # HTTP header 'Accept' (if needed)
1166
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1167
+
1168
+ # form parameters
1169
+ form_params = opts[:form_params] || {}
1170
+
1171
+ # http body (model)
1172
+ post_body = opts[:debug_body]
1173
+
1174
+ # return_type
1175
+ return_type = opts[:debug_return_type] || 'ListNearbyTechnicians200Response'
1176
+
1177
+ # auth_names
1178
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1179
+
1180
+ new_options = opts.merge(
1181
+ :operation => :"JobRequestBusinessApi.list_nearby_technicians",
1182
+ :header_params => header_params,
1183
+ :query_params => query_params,
1184
+ :form_params => form_params,
1185
+ :body => post_body,
1186
+ :auth_names => auth_names,
1187
+ :return_type => return_type
1188
+ )
1189
+
1190
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
1191
+ if @api_client.config.debugging
1192
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#list_nearby_technicians\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
1193
+ end
1194
+ return data, status_code, headers
1195
+ end
1196
+
1197
+ # Preview re-staffing a technician's whole day (sick call)
1198
+ # Solves (WITHOUT writing) the re-staffing of every job on a technician's board for a date range: each job is handed to an ALTERNATE lead technician at its UNCHANGED window — the customer's appointment never moves, two overlapping jobs never land on the same alternate, and the absent technician is never a candidate. `date`/`until_date` are business-local calendar days (inclusive, ≤ 14 days). Jobs the planner cannot re-staff come back in `unresolved` with a `reason_code` (no qualified technician free / crew job / multi-day job / already in progress) — a partial plan is a normal 200, not an error. `solver.duration_ms` is server-side planner time; `solver.deterministic` is true (same input ⇒ same plan). Read-only, safe to repeat; copy `resolved[]` into the commit body. No time-off record is required to preview. When the strict pass leaves a job unresolved, its row ALSO carries `alternatives[]` — the relaxation ladder's priced options, cheapest constraint first (ABSENCE_RESOLVE_DESIGN.md §9): `reassign_out_of_area` (same window, a lead outside the job's zone — cost.distance_km/travel_minutes), then `reschedule_same_day` / `reschedule_later_day` (the earliest free window on a qualified lead, in-area before out-of-area, up to 3 working days past until_date — cost.customer_renotified, cost.day_offset, cost.sla_breached). Each option is a PROPOSAL: nothing is applied until the coordinator copies it into the commit body with its `alternative_kind` (+ `start_at`/`end_at` for a reschedule). `alternatives` is an empty array when even the ladder found nothing; `solver.alternatives_truncated` is true when the ladder's time budget cut the search short. Displacing another customer's job and overtime are deliberately NOT offered. See ABSENCE_RESOLVE_DESIGN.md.
1199
+ # @param job_request_absence_preview_request [JobRequestAbsencePreviewRequest] absent technician + date range (+ optional job subset)
1200
+ # @param [Hash] opts the optional parameters
1201
+ # @return [CommitAbsenceResolve200Response]
1202
+ def preview_absence_resolve(job_request_absence_preview_request, opts = {})
1203
+ data, _status_code, _headers = preview_absence_resolve_with_http_info(job_request_absence_preview_request, opts)
1204
+ data
1205
+ end
1206
+
1207
+ # Preview re-staffing a technician&#39;s whole day (sick call)
1208
+ # Solves (WITHOUT writing) the re-staffing of every job on a technician&#39;s board for a date range: each job is handed to an ALTERNATE lead technician at its UNCHANGED window — the customer&#39;s appointment never moves, two overlapping jobs never land on the same alternate, and the absent technician is never a candidate. &#x60;date&#x60;/&#x60;until_date&#x60; are business-local calendar days (inclusive, ≤ 14 days). Jobs the planner cannot re-staff come back in &#x60;unresolved&#x60; with a &#x60;reason_code&#x60; (no qualified technician free / crew job / multi-day job / already in progress) — a partial plan is a normal 200, not an error. &#x60;solver.duration_ms&#x60; is server-side planner time; &#x60;solver.deterministic&#x60; is true (same input ⇒ same plan). Read-only, safe to repeat; copy &#x60;resolved[]&#x60; into the commit body. No time-off record is required to preview. When the strict pass leaves a job unresolved, its row ALSO carries &#x60;alternatives[]&#x60; — the relaxation ladder&#39;s priced options, cheapest constraint first (ABSENCE_RESOLVE_DESIGN.md §9): &#x60;reassign_out_of_area&#x60; (same window, a lead outside the job&#39;s zone — cost.distance_km/travel_minutes), then &#x60;reschedule_same_day&#x60; / &#x60;reschedule_later_day&#x60; (the earliest free window on a qualified lead, in-area before out-of-area, up to 3 working days past until_date — cost.customer_renotified, cost.day_offset, cost.sla_breached). Each option is a PROPOSAL: nothing is applied until the coordinator copies it into the commit body with its &#x60;alternative_kind&#x60; (+ &#x60;start_at&#x60;/&#x60;end_at&#x60; for a reschedule). &#x60;alternatives&#x60; is an empty array when even the ladder found nothing; &#x60;solver.alternatives_truncated&#x60; is true when the ladder&#39;s time budget cut the search short. Displacing another customer&#39;s job and overtime are deliberately NOT offered. See ABSENCE_RESOLVE_DESIGN.md.
1209
+ # @param job_request_absence_preview_request [JobRequestAbsencePreviewRequest] absent technician + date range (+ optional job subset)
1210
+ # @param [Hash] opts the optional parameters
1211
+ # @return [Array<(CommitAbsenceResolve200Response, Integer, Hash)>] CommitAbsenceResolve200Response data, response status code and response headers
1212
+ def preview_absence_resolve_with_http_info(job_request_absence_preview_request, opts = {})
1213
+ if @api_client.config.debugging
1214
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.preview_absence_resolve ...'
1215
+ end
1216
+ # verify the required parameter 'job_request_absence_preview_request' is set
1217
+ if @api_client.config.client_side_validation && job_request_absence_preview_request.nil?
1218
+ fail ArgumentError, "Missing the required parameter 'job_request_absence_preview_request' when calling JobRequestBusinessApi.preview_absence_resolve"
1219
+ end
1220
+ # resource path
1221
+ local_var_path = '/job-requests/absence/preview'
1222
+
1223
+ # query parameters
1224
+ query_params = opts[:query_params] || {}
1225
+
1226
+ # header parameters
1227
+ header_params = opts[:header_params] || {}
1228
+ # HTTP header 'Accept' (if needed)
1229
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1230
+ # HTTP header 'Content-Type'
1231
+ content_type = @api_client.select_header_content_type(['application/json'])
1232
+ if !content_type.nil?
1233
+ header_params['Content-Type'] = content_type
1234
+ end
1235
+
1236
+ # form parameters
1237
+ form_params = opts[:form_params] || {}
1238
+
1239
+ # http body (model)
1240
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_absence_preview_request)
1241
+
1242
+ # return_type
1243
+ return_type = opts[:debug_return_type] || 'CommitAbsenceResolve200Response'
1244
+
1245
+ # auth_names
1246
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1247
+
1248
+ new_options = opts.merge(
1249
+ :operation => :"JobRequestBusinessApi.preview_absence_resolve",
1250
+ :header_params => header_params,
1251
+ :query_params => query_params,
1252
+ :form_params => form_params,
1253
+ :body => post_body,
1254
+ :auth_names => auth_names,
1255
+ :return_type => return_type
1256
+ )
1257
+
1258
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
1259
+ if @api_client.config.debugging
1260
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#preview_absence_resolve\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
1261
+ end
1262
+ return data, status_code, headers
1263
+ end
1264
+
1265
+ # Preview emergency insert + cascade reschedule
1266
+ # Computes (WITHOUT writing) the cascade of inserting an emergency job onto a technician at a chosen time: where the emergency lands + every job pushed back, grouped per business-local day. `displacement_mode=reassign` instead hands each displaced job to another feasible technician at its ORIGINAL window (same-day promise) — jobs with no alternate capacity fall back to reschedule and stay in `days`. `mode=overtime` keeps everyone same-day (tech works late); `mode=next_day` rolls overflow to the next working day(s). On a day the business does not work the preview does NOT need `after_hours_override`: it plans as the override would and returns `after_hours_override_required: true` plus an AFTER_HOURS warning — ask the coordinator whether they have phoned the technician, then commit with after_hours_override=true (the commit refuses without it). This endpoint never runs the technician's working-hours/time-off feasibility check (that only happens on /candidates) — it validates the named technician exists and builds the cascade. Read-only — safe to call repeatedly; commit is a separate endpoint. Isolated feature (see EMERGENCY_RESCHEDULE_DESIGN.md). 409 NEXT STEPS: EMERGENCY_RESCHEDULE_SLOT_OCCUPIED — the landing window is blocked by a job the cascade may NOT move (another P0, a crew or multi-day job); `data.conflicts[]` names each blocking job (short_code/start_at/end_at/frozen_because): choose another technician (walk the /candidates ranking) or another time; displacement never touches P0/crew/multi-day anchors. EMERGENCY_RESCHEDULE_NOT_ELIGIBLE / CREW_UNSUPPORTED / MULTIDAY_UNSUPPORTED / IN_PAST — same remedies and `data` shapes as /candidates. EMERGENCY_RESCHEDULE_NO_WORKING_DAY is never returned by the preview: since 2026-10-03 a closed day answers 200 with after_hours_override_required=true instead.
1267
+ # @param job_request_emergency_preview_request [JobRequestEmergencyPreviewRequest] emergency insert spec
1268
+ # @param [Hash] opts the optional parameters
1269
+ # @return [CommitEmergencyReschedule200Response]
1270
+ def preview_emergency_reschedule(job_request_emergency_preview_request, opts = {})
1271
+ data, _status_code, _headers = preview_emergency_reschedule_with_http_info(job_request_emergency_preview_request, opts)
1272
+ data
1273
+ end
1274
+
1275
+ # Preview emergency insert + cascade reschedule
1276
+ # Computes (WITHOUT writing) the cascade of inserting an emergency job onto a technician at a chosen time: where the emergency lands + every job pushed back, grouped per business-local day. &#x60;displacement_mode&#x3D;reassign&#x60; instead hands each displaced job to another feasible technician at its ORIGINAL window (same-day promise) — jobs with no alternate capacity fall back to reschedule and stay in &#x60;days&#x60;. &#x60;mode&#x3D;overtime&#x60; keeps everyone same-day (tech works late); &#x60;mode&#x3D;next_day&#x60; rolls overflow to the next working day(s). On a day the business does not work the preview does NOT need &#x60;after_hours_override&#x60;: it plans as the override would and returns &#x60;after_hours_override_required: true&#x60; plus an AFTER_HOURS warning — ask the coordinator whether they have phoned the technician, then commit with after_hours_override&#x3D;true (the commit refuses without it). This endpoint never runs the technician&#39;s working-hours/time-off feasibility check (that only happens on /candidates) — it validates the named technician exists and builds the cascade. Read-only — safe to call repeatedly; commit is a separate endpoint. Isolated feature (see EMERGENCY_RESCHEDULE_DESIGN.md). 409 NEXT STEPS: EMERGENCY_RESCHEDULE_SLOT_OCCUPIED — the landing window is blocked by a job the cascade may NOT move (another P0, a crew or multi-day job); &#x60;data.conflicts[]&#x60; names each blocking job (short_code/start_at/end_at/frozen_because): choose another technician (walk the /candidates ranking) or another time; displacement never touches P0/crew/multi-day anchors. EMERGENCY_RESCHEDULE_NOT_ELIGIBLE / CREW_UNSUPPORTED / MULTIDAY_UNSUPPORTED / IN_PAST — same remedies and &#x60;data&#x60; shapes as /candidates. EMERGENCY_RESCHEDULE_NO_WORKING_DAY is never returned by the preview: since 2026-10-03 a closed day answers 200 with after_hours_override_required&#x3D;true instead.
1277
+ # @param job_request_emergency_preview_request [JobRequestEmergencyPreviewRequest] emergency insert spec
1278
+ # @param [Hash] opts the optional parameters
1279
+ # @return [Array<(CommitEmergencyReschedule200Response, Integer, Hash)>] CommitEmergencyReschedule200Response data, response status code and response headers
1280
+ def preview_emergency_reschedule_with_http_info(job_request_emergency_preview_request, opts = {})
1281
+ if @api_client.config.debugging
1282
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.preview_emergency_reschedule ...'
1283
+ end
1284
+ # verify the required parameter 'job_request_emergency_preview_request' is set
1285
+ if @api_client.config.client_side_validation && job_request_emergency_preview_request.nil?
1286
+ fail ArgumentError, "Missing the required parameter 'job_request_emergency_preview_request' when calling JobRequestBusinessApi.preview_emergency_reschedule"
1287
+ end
1288
+ # resource path
1289
+ local_var_path = '/job-requests/emergency/preview'
1290
+
1291
+ # query parameters
1292
+ query_params = opts[:query_params] || {}
1293
+
1294
+ # header parameters
1295
+ header_params = opts[:header_params] || {}
1296
+ # HTTP header 'Accept' (if needed)
1297
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1298
+ # HTTP header 'Content-Type'
1299
+ content_type = @api_client.select_header_content_type(['application/json'])
1300
+ if !content_type.nil?
1301
+ header_params['Content-Type'] = content_type
1302
+ end
1303
+
1304
+ # form parameters
1305
+ form_params = opts[:form_params] || {}
1306
+
1307
+ # http body (model)
1308
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_emergency_preview_request)
1309
+
1310
+ # return_type
1311
+ return_type = opts[:debug_return_type] || 'CommitEmergencyReschedule200Response'
1312
+
1313
+ # auth_names
1314
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1315
+
1316
+ new_options = opts.merge(
1317
+ :operation => :"JobRequestBusinessApi.preview_emergency_reschedule",
1318
+ :header_params => header_params,
1319
+ :query_params => query_params,
1320
+ :form_params => form_params,
1321
+ :body => post_body,
1322
+ :auth_names => auth_names,
1323
+ :return_type => return_type
1324
+ )
1325
+
1326
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
1327
+ if @api_client.config.debugging
1328
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#preview_emergency_reschedule\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
1329
+ end
1330
+ return data, status_code, headers
1331
+ end
1332
+
1333
+ # Preview a schedule-board job move
1334
+ # Computes (WITHOUT writing) the outcome of moving a confirmed job to a new time and/or technician: where it lands, every later job pushed back per `mode`, and the warnings the coordinator would accept (displaced jobs leaving their confirmed windows, overtime). Same technician = pure time move; different technician = manual reassign. Read-only — safe to call repeatedly while dragging; commit is a separate endpoint. On a day the business does not work, a p0 single-person job previews WITHOUT after_hours_override: the plan is computed as the override would place it and the response carries `after_hours_override_required: true` — ask the coordinator whether they have phoned the technician, then commit with after_hours_override=true. Any other job is refused with SCHEDULE_MOVE_NO_WORKING_DAY and `data.override_available: false`. See SCHEDULE_BOARD_DESIGN.md. Warning detail: a TECH_NOT_FEASIBLE warning carries `reason` (= `blockers[0].kind`) = `outside_service_area` | `missing_required_skills` | `not_lead_tier` | `no_working_day` | `on_time_off` | `off_shift` | `visit_too_long` | `cannot_arrive_in_time` (commute from the tech day-start location / shift start; `earliest_feasible_at`, RFC3339 UTC, is the first same-day time they CAN be on site — suggest it as the drop slot) | `not_available_today` (diagnosis unavailable). `blockers[]` names EVERY hard filter that failed, most-structural first — a client reading only `reason` still works. For a P0 move this warning is advisory (coordinator may commit anyway) and carries the SAME `blockers[]` a p1/p2/p3 move would get as the hard 409 SCHEDULE_MOVE_TECH_INFEASIBLE below. 409 NEXT STEPS: SCHEDULE_MOVE_NOT_ELIGIBLE (job unconfirmed/unquoted/archived/completed — `data.failed_precondition` names which) · SCHEDULE_MOVE_IN_PROGRESS (tech already executing — `data.fired_actions[]` lists the actions already fired; do not move) · SCHEDULE_MOVE_IN_PAST (pick a future time — its `data` carries `business_timezone`, the naive `start_at` and the `start_at_utc` it resolved to, which is what tells a caller whose own clock says otherwise where the difference came from) · SCHEDULE_MOVE_SLOT_OCCUPIED (landing window blocked by an immovable anchor; `data.conflicts[]` names it — another tech/time) · SCHEDULE_MOVE_TECH_INFEASIBLE (non-P0 hard block: target tech not qualified/available — its `data` carries `technician` (id+name), `reason` (same catalog as the TECH_NOT_FEASIBLE warning above), `blockers[]` (every cause, most-structural first) and, for `cannot_arrive_in_time`, `earliest_feasible_at` (RFC3339 UTC) to suggest as the drop slot; change tech or time) · SCHEDULE_MOVE_MULTIDAY_UNSUPPORTED (multi-day jobs not movable v1 — `data.session_count`/`data.session_dates`) · SCHEDULE_MOVE_NO_WORKING_DAY (`data.business_timezone`/`data.requested_weekday` — pick a working day, or set after_hours_override=true for a P0 whose technician has been phoned) · SCHEDULE_MOVE_REQUIRES_FREE_SLOT (non-P0 moves may not displace — `data.would_push[]` names the jobs that would be pushed, `data.allow_non_p0_displacement: false` names the setting that would permit it, unless the crew case sets `data.crew_never_displaces: true` instead — free capacity only) · SCHEDULE_MOVE_CREW_UNSTAFFABLE (a crew slot has no feasible replacement at the new time — another time). A landing outside the customer-confirmed window is NOT an error — it returns 200 with a MOVED_OUTSIDE_WINDOW warning (customer_window attached) that the coordinator overrides.
1335
+ # @param id [String] Job request ID (UUID or short_code)
1336
+ # @param job_request_move_preview_req [JobRequestMovePreviewReq] move spec
1337
+ # @param [Hash] opts the optional parameters
1338
+ # @return [CommitJobRequestMove200Response]
1339
+ def preview_job_request_move(id, job_request_move_preview_req, opts = {})
1340
+ data, _status_code, _headers = preview_job_request_move_with_http_info(id, job_request_move_preview_req, opts)
1341
+ data
1342
+ end
1343
+
1344
+ # Preview a schedule-board job move
1345
+ # Computes (WITHOUT writing) the outcome of moving a confirmed job to a new time and/or technician: where it lands, every later job pushed back per &#x60;mode&#x60;, and the warnings the coordinator would accept (displaced jobs leaving their confirmed windows, overtime). Same technician &#x3D; pure time move; different technician &#x3D; manual reassign. Read-only — safe to call repeatedly while dragging; commit is a separate endpoint. On a day the business does not work, a p0 single-person job previews WITHOUT after_hours_override: the plan is computed as the override would place it and the response carries &#x60;after_hours_override_required: true&#x60; — ask the coordinator whether they have phoned the technician, then commit with after_hours_override&#x3D;true. Any other job is refused with SCHEDULE_MOVE_NO_WORKING_DAY and &#x60;data.override_available: false&#x60;. See SCHEDULE_BOARD_DESIGN.md. Warning detail: a TECH_NOT_FEASIBLE warning carries &#x60;reason&#x60; (&#x3D; &#x60;blockers[0].kind&#x60;) &#x3D; &#x60;outside_service_area&#x60; | &#x60;missing_required_skills&#x60; | &#x60;not_lead_tier&#x60; | &#x60;no_working_day&#x60; | &#x60;on_time_off&#x60; | &#x60;off_shift&#x60; | &#x60;visit_too_long&#x60; | &#x60;cannot_arrive_in_time&#x60; (commute from the tech day-start location / shift start; &#x60;earliest_feasible_at&#x60;, RFC3339 UTC, is the first same-day time they CAN be on site — suggest it as the drop slot) | &#x60;not_available_today&#x60; (diagnosis unavailable). &#x60;blockers[]&#x60; names EVERY hard filter that failed, most-structural first — a client reading only &#x60;reason&#x60; still works. For a P0 move this warning is advisory (coordinator may commit anyway) and carries the SAME &#x60;blockers[]&#x60; a p1/p2/p3 move would get as the hard 409 SCHEDULE_MOVE_TECH_INFEASIBLE below. 409 NEXT STEPS: SCHEDULE_MOVE_NOT_ELIGIBLE (job unconfirmed/unquoted/archived/completed — &#x60;data.failed_precondition&#x60; names which) · SCHEDULE_MOVE_IN_PROGRESS (tech already executing — &#x60;data.fired_actions[]&#x60; lists the actions already fired; do not move) · SCHEDULE_MOVE_IN_PAST (pick a future time — its &#x60;data&#x60; carries &#x60;business_timezone&#x60;, the naive &#x60;start_at&#x60; and the &#x60;start_at_utc&#x60; it resolved to, which is what tells a caller whose own clock says otherwise where the difference came from) · SCHEDULE_MOVE_SLOT_OCCUPIED (landing window blocked by an immovable anchor; &#x60;data.conflicts[]&#x60; names it — another tech/time) · SCHEDULE_MOVE_TECH_INFEASIBLE (non-P0 hard block: target tech not qualified/available — its &#x60;data&#x60; carries &#x60;technician&#x60; (id+name), &#x60;reason&#x60; (same catalog as the TECH_NOT_FEASIBLE warning above), &#x60;blockers[]&#x60; (every cause, most-structural first) and, for &#x60;cannot_arrive_in_time&#x60;, &#x60;earliest_feasible_at&#x60; (RFC3339 UTC) to suggest as the drop slot; change tech or time) · SCHEDULE_MOVE_MULTIDAY_UNSUPPORTED (multi-day jobs not movable v1 — &#x60;data.session_count&#x60;/&#x60;data.session_dates&#x60;) · SCHEDULE_MOVE_NO_WORKING_DAY (&#x60;data.business_timezone&#x60;/&#x60;data.requested_weekday&#x60; — pick a working day, or set after_hours_override&#x3D;true for a P0 whose technician has been phoned) · SCHEDULE_MOVE_REQUIRES_FREE_SLOT (non-P0 moves may not displace — &#x60;data.would_push[]&#x60; names the jobs that would be pushed, &#x60;data.allow_non_p0_displacement: false&#x60; names the setting that would permit it, unless the crew case sets &#x60;data.crew_never_displaces: true&#x60; instead — free capacity only) · SCHEDULE_MOVE_CREW_UNSTAFFABLE (a crew slot has no feasible replacement at the new time — another time). A landing outside the customer-confirmed window is NOT an error — it returns 200 with a MOVED_OUTSIDE_WINDOW warning (customer_window attached) that the coordinator overrides.
1346
+ # @param id [String] Job request ID (UUID or short_code)
1347
+ # @param job_request_move_preview_req [JobRequestMovePreviewReq] move spec
1348
+ # @param [Hash] opts the optional parameters
1349
+ # @return [Array<(CommitJobRequestMove200Response, Integer, Hash)>] CommitJobRequestMove200Response data, response status code and response headers
1350
+ def preview_job_request_move_with_http_info(id, job_request_move_preview_req, opts = {})
1351
+ if @api_client.config.debugging
1352
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.preview_job_request_move ...'
1353
+ end
1354
+ # verify the required parameter 'id' is set
1355
+ if @api_client.config.client_side_validation && id.nil?
1356
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.preview_job_request_move"
1357
+ end
1358
+ # verify the required parameter 'job_request_move_preview_req' is set
1359
+ if @api_client.config.client_side_validation && job_request_move_preview_req.nil?
1360
+ fail ArgumentError, "Missing the required parameter 'job_request_move_preview_req' when calling JobRequestBusinessApi.preview_job_request_move"
1361
+ end
1362
+ # resource path
1363
+ local_var_path = '/job-requests/{id}/move/preview'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
1364
+
1365
+ # query parameters
1366
+ query_params = opts[:query_params] || {}
1367
+
1368
+ # header parameters
1369
+ header_params = opts[:header_params] || {}
1370
+ # HTTP header 'Accept' (if needed)
1371
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1372
+ # HTTP header 'Content-Type'
1373
+ content_type = @api_client.select_header_content_type(['application/json'])
1374
+ if !content_type.nil?
1375
+ header_params['Content-Type'] = content_type
1376
+ end
1377
+
1378
+ # form parameters
1379
+ form_params = opts[:form_params] || {}
1380
+
1381
+ # http body (model)
1382
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_move_preview_req)
1383
+
1384
+ # return_type
1385
+ return_type = opts[:debug_return_type] || 'CommitJobRequestMove200Response'
1386
+
1387
+ # auth_names
1388
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1389
+
1390
+ new_options = opts.merge(
1391
+ :operation => :"JobRequestBusinessApi.preview_job_request_move",
1392
+ :header_params => header_params,
1393
+ :query_params => query_params,
1394
+ :form_params => form_params,
1395
+ :body => post_body,
1396
+ :auth_names => auth_names,
1397
+ :return_type => return_type
1398
+ )
1399
+
1400
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
1401
+ if @api_client.config.debugging
1402
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#preview_job_request_move\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
1403
+ end
1404
+ return data, status_code, headers
1405
+ end
1406
+
1407
+ # Fire quote (FIXED action — business)
1408
+ # Sends the quote: sets quoted_at + duration cols, advances pending_action to confirm_booking. Status stays `booking`. job_duration_minutes may be omitted when the job's job type has a default_duration_minutes: the type's default duration and buffers are used (a buffer you send still wins). This is how an automation or voice agent schedules work it cannot size. Before writing, checks that the customer will see at least one slot: the same engine as the customer slot picker runs over the windows the customer asked for with THIS quote's duration (working hours, service areas, time-off, existing bookings, crew coverage). If no slot exists the quote is refused with 409 JOB_REQUEST_QUOTE_NOT_SCHEDULABLE; data.reason says why (outside_working_hours, requested_windows_passed, outside_service_area, off_shift, on_time_off, missing_required_skills, no_technician_available, ...) and data.blocked_by counts the roster per blocker. Send force=true to schedule it anyway after agreeing a time with the customer.
1409
+ # @param id [String] Job request ID
1410
+ # @param job_request_quote_request [JobRequestQuoteRequest] Quote payload
1411
+ # @param [Hash] opts the optional parameters
1412
+ # @return [QuoteJobRequest200Response]
1413
+ def quote_job_request(id, job_request_quote_request, opts = {})
1414
+ data, _status_code, _headers = quote_job_request_with_http_info(id, job_request_quote_request, opts)
1415
+ data
1416
+ end
1417
+
1418
+ # Fire quote (FIXED action — business)
1419
+ # Sends the quote: sets quoted_at + duration cols, advances pending_action to confirm_booking. Status stays &#x60;booking&#x60;. job_duration_minutes may be omitted when the job&#39;s job type has a default_duration_minutes: the type&#39;s default duration and buffers are used (a buffer you send still wins). This is how an automation or voice agent schedules work it cannot size. Before writing, checks that the customer will see at least one slot: the same engine as the customer slot picker runs over the windows the customer asked for with THIS quote&#39;s duration (working hours, service areas, time-off, existing bookings, crew coverage). If no slot exists the quote is refused with 409 JOB_REQUEST_QUOTE_NOT_SCHEDULABLE; data.reason says why (outside_working_hours, requested_windows_passed, outside_service_area, off_shift, on_time_off, missing_required_skills, no_technician_available, ...) and data.blocked_by counts the roster per blocker. Send force&#x3D;true to schedule it anyway after agreeing a time with the customer.
1420
+ # @param id [String] Job request ID
1421
+ # @param job_request_quote_request [JobRequestQuoteRequest] Quote payload
1422
+ # @param [Hash] opts the optional parameters
1423
+ # @return [Array<(QuoteJobRequest200Response, Integer, Hash)>] QuoteJobRequest200Response data, response status code and response headers
1424
+ def quote_job_request_with_http_info(id, job_request_quote_request, opts = {})
1425
+ if @api_client.config.debugging
1426
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.quote_job_request ...'
1427
+ end
1428
+ # verify the required parameter 'id' is set
1429
+ if @api_client.config.client_side_validation && id.nil?
1430
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.quote_job_request"
1431
+ end
1432
+ # verify the required parameter 'job_request_quote_request' is set
1433
+ if @api_client.config.client_side_validation && job_request_quote_request.nil?
1434
+ fail ArgumentError, "Missing the required parameter 'job_request_quote_request' when calling JobRequestBusinessApi.quote_job_request"
1435
+ end
1436
+ # resource path
1437
+ local_var_path = '/job-requests/{id}/quote'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
1438
+
1439
+ # query parameters
1440
+ query_params = opts[:query_params] || {}
1441
+
1442
+ # header parameters
1443
+ header_params = opts[:header_params] || {}
1444
+ # HTTP header 'Accept' (if needed)
1445
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1446
+ # HTTP header 'Content-Type'
1447
+ content_type = @api_client.select_header_content_type(['application/json'])
1448
+ if !content_type.nil?
1449
+ header_params['Content-Type'] = content_type
1450
+ end
1451
+
1452
+ # form parameters
1453
+ form_params = opts[:form_params] || {}
1454
+
1455
+ # http body (model)
1456
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_quote_request)
1457
+
1458
+ # return_type
1459
+ return_type = opts[:debug_return_type] || 'QuoteJobRequest200Response'
1460
+
1461
+ # auth_names
1462
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1463
+
1464
+ new_options = opts.merge(
1465
+ :operation => :"JobRequestBusinessApi.quote_job_request",
1466
+ :header_params => header_params,
1467
+ :query_params => query_params,
1468
+ :form_params => form_params,
1469
+ :body => post_body,
1470
+ :auth_names => auth_names,
1471
+ :return_type => return_type
1472
+ )
1473
+
1474
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
1475
+ if @api_client.config.debugging
1476
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#quote_job_request\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
1477
+ end
1478
+ return data, status_code, headers
1479
+ end
1480
+
1481
+ # Set job priority (scheduling staff)
1482
+ # Sets the P0–P3 priority on a non-archived, non-completed job (Owner / Administrator / Booking Coordinator). Allowed values: \"p0\" (emergency, interrupt-driven) | \"p1\" (top — displaced only by p0; may carry an sla_deadline arming auto-escalation) | \"p2\" (standard) | \"p3\" (deferrable, first displacement victim). sla_deadline is only valid with p1 and must be in the future (business-local naive datetime); moving away from p1 disarms the SLA clock. Accepts UUID or short_code in :id. Lowering a job to anything but p0 never moves it: when it still sits outside the business's working hours (typically placed there earlier through the after-hours override) the response carries an AFTER_HOURS warning per affected session, and the job should be moved back to a working day.
1483
+ # @param id [String] Job request ID or short_code
1484
+ # @param job_request_update_priority_request [JobRequestUpdatePriorityRequest] Priority payload
1485
+ # @param [Hash] opts the optional parameters
1486
+ # @return [ResponseEnvelope]
1487
+ def update_job_priority(id, job_request_update_priority_request, opts = {})
1488
+ data, _status_code, _headers = update_job_priority_with_http_info(id, job_request_update_priority_request, opts)
1489
+ data
1490
+ end
1491
+
1492
+ # Set job priority (scheduling staff)
1493
+ # Sets the P0–P3 priority on a non-archived, non-completed job (Owner / Administrator / Booking Coordinator). Allowed values: \&quot;p0\&quot; (emergency, interrupt-driven) | \&quot;p1\&quot; (top — displaced only by p0; may carry an sla_deadline arming auto-escalation) | \&quot;p2\&quot; (standard) | \&quot;p3\&quot; (deferrable, first displacement victim). sla_deadline is only valid with p1 and must be in the future (business-local naive datetime); moving away from p1 disarms the SLA clock. Accepts UUID or short_code in :id. Lowering a job to anything but p0 never moves it: when it still sits outside the business&#39;s working hours (typically placed there earlier through the after-hours override) the response carries an AFTER_HOURS warning per affected session, and the job should be moved back to a working day.
1494
+ # @param id [String] Job request ID or short_code
1495
+ # @param job_request_update_priority_request [JobRequestUpdatePriorityRequest] Priority payload
1496
+ # @param [Hash] opts the optional parameters
1497
+ # @return [Array<(ResponseEnvelope, Integer, Hash)>] ResponseEnvelope data, response status code and response headers
1498
+ def update_job_priority_with_http_info(id, job_request_update_priority_request, opts = {})
1499
+ if @api_client.config.debugging
1500
+ @api_client.config.logger.debug 'Calling API: JobRequestBusinessApi.update_job_priority ...'
1501
+ end
1502
+ # verify the required parameter 'id' is set
1503
+ if @api_client.config.client_side_validation && id.nil?
1504
+ fail ArgumentError, "Missing the required parameter 'id' when calling JobRequestBusinessApi.update_job_priority"
1505
+ end
1506
+ # verify the required parameter 'job_request_update_priority_request' is set
1507
+ if @api_client.config.client_side_validation && job_request_update_priority_request.nil?
1508
+ fail ArgumentError, "Missing the required parameter 'job_request_update_priority_request' when calling JobRequestBusinessApi.update_job_priority"
1509
+ end
1510
+ # resource path
1511
+ local_var_path = '/job-requests/{id}/priority'.sub('{' + 'id' + '}', CGI.escape(id.to_s))
1512
+
1513
+ # query parameters
1514
+ query_params = opts[:query_params] || {}
1515
+
1516
+ # header parameters
1517
+ header_params = opts[:header_params] || {}
1518
+ # HTTP header 'Accept' (if needed)
1519
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
1520
+ # HTTP header 'Content-Type'
1521
+ content_type = @api_client.select_header_content_type(['application/json'])
1522
+ if !content_type.nil?
1523
+ header_params['Content-Type'] = content_type
1524
+ end
1525
+
1526
+ # form parameters
1527
+ form_params = opts[:form_params] || {}
1528
+
1529
+ # http body (model)
1530
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(job_request_update_priority_request)
1531
+
1532
+ # return_type
1533
+ return_type = opts[:debug_return_type] || 'ResponseEnvelope'
1534
+
1535
+ # auth_names
1536
+ auth_names = opts[:debug_auth_names] || ['ApiKeyAuth']
1537
+
1538
+ new_options = opts.merge(
1539
+ :operation => :"JobRequestBusinessApi.update_job_priority",
1540
+ :header_params => header_params,
1541
+ :query_params => query_params,
1542
+ :form_params => form_params,
1543
+ :body => post_body,
1544
+ :auth_names => auth_names,
1545
+ :return_type => return_type
1546
+ )
1547
+
1548
+ data, status_code, headers = @api_client.call_api(:PATCH, local_var_path, new_options)
1549
+ if @api_client.config.debugging
1550
+ @api_client.config.logger.debug "API called: JobRequestBusinessApi#update_job_priority\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
445
1551
  end
446
1552
  return data, status_code, headers
447
1553
  end