@fleetbase/fleetops-engine 0.6.65 → 0.6.66

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 (1051) hide show
  1. package/RELEASE.md +11 -6
  2. package/addon/components/ai/create-order-preview.hbs +10 -5
  3. package/addon/components/asset/pill.hbs +24 -0
  4. package/addon/components/asset/summary.hbs +1 -0
  5. package/addon/components/asset-connection/pill.hbs +24 -0
  6. package/addon/components/asset-connection/summary.hbs +1 -0
  7. package/addon/components/attachable/pill.hbs +24 -0
  8. package/addon/components/attachable/summary.hbs +1 -0
  9. package/addon/components/cell/asset-connection-identity.hbs +1 -0
  10. package/addon/components/cell/asset-identity.hbs +1 -0
  11. package/addon/components/cell/attachable-identity.hbs +1 -0
  12. package/addon/components/cell/contact-identity.hbs +1 -0
  13. package/addon/components/cell/customer-identity.hbs +1 -0
  14. package/addon/components/cell/device-event-identity.hbs +1 -0
  15. package/addon/components/cell/device-identity.hbs +1 -24
  16. package/addon/components/cell/driver-identity.hbs +1 -33
  17. package/addon/components/cell/entity-identity.hbs +1 -0
  18. package/addon/components/cell/equipment-identity.hbs +1 -5
  19. package/addon/components/cell/facilitator-identity.hbs +1 -0
  20. package/addon/components/cell/fleet-driver-identity.hbs +1 -0
  21. package/addon/components/cell/fleet-identity.hbs +1 -0
  22. package/addon/components/cell/fuel-provider-connection-identity.hbs +1 -0
  23. package/addon/components/cell/fuel-provider-transaction-identity.hbs +1 -0
  24. package/addon/components/cell/fuel-report-identity.hbs +1 -0
  25. package/addon/components/cell/inspection-form-identity.hbs +1 -0
  26. package/addon/components/cell/inspection-item-result-identity.hbs +1 -0
  27. package/addon/components/cell/inspection-submission-identity.hbs +1 -0
  28. package/addon/components/cell/integrated-vendor-identity.hbs +1 -0
  29. package/addon/components/cell/issue-identity.hbs +1 -0
  30. package/addon/components/cell/maintenance-identity.hbs +1 -0
  31. package/addon/components/cell/maintenance-schedule-identity.hbs +1 -0
  32. package/addon/components/cell/maintenance-subject-identity.hbs +1 -0
  33. package/addon/components/cell/manifest-identity.hbs +1 -0
  34. package/addon/components/cell/manifest-stop-identity.hbs +1 -0
  35. package/addon/components/cell/order-config-identity.hbs +1 -0
  36. package/addon/components/cell/order-identity.hbs +1 -0
  37. package/addon/components/cell/part-identity.hbs +1 -5
  38. package/addon/components/cell/payload-identity.hbs +1 -0
  39. package/addon/components/cell/place-identity.hbs +1 -0
  40. package/addon/components/cell/position-identity.hbs +1 -0
  41. package/addon/components/cell/purchase-rate-identity.hbs +1 -0
  42. package/addon/components/cell/recurring-order-schedule-identity.hbs +1 -0
  43. package/addon/components/cell/resource-list.hbs +10 -0
  44. package/addon/components/cell/resource-list.js +28 -0
  45. package/addon/components/cell/route-identity.hbs +1 -0
  46. package/addon/components/cell/sensor-identity.hbs +1 -0
  47. package/addon/components/cell/service-area-identity.hbs +1 -0
  48. package/addon/components/cell/service-quote-identity.hbs +1 -0
  49. package/addon/components/cell/service-quote-item-identity.hbs +1 -0
  50. package/addon/components/cell/service-rate-fee-identity.hbs +1 -0
  51. package/addon/components/cell/service-rate-identity.hbs +1 -0
  52. package/addon/components/cell/service-rate-parcel-fee-identity.hbs +1 -0
  53. package/addon/components/cell/telematic-identity.hbs +1 -0
  54. package/addon/components/cell/tracking-number-identity.hbs +1 -0
  55. package/addon/components/cell/tracking-status-identity.hbs +1 -0
  56. package/addon/components/cell/trailer-identity.hbs +1 -0
  57. package/addon/components/cell/vehicle-device-identity.hbs +1 -0
  58. package/addon/components/cell/vehicle-identity.hbs +1 -33
  59. package/addon/components/cell/vendor-identity.hbs +1 -0
  60. package/addon/components/cell/warranty-identity.hbs +1 -0
  61. package/addon/components/cell/waypoint-identity.hbs +1 -0
  62. package/addon/components/cell/work-order-identity.hbs +1 -0
  63. package/addon/components/cell/zone-identity.hbs +1 -0
  64. package/addon/components/contact/form.hbs +2 -4
  65. package/addon/components/contact/pill.hbs +24 -0
  66. package/addon/components/contact/summary.hbs +1 -0
  67. package/addon/components/customer/create-order-form.hbs +8 -4
  68. package/addon/components/customer/form.hbs +2 -4
  69. package/addon/components/customer/order-form.hbs +10 -8
  70. package/addon/components/customer/pill.hbs +24 -0
  71. package/addon/components/customer/summary.hbs +1 -0
  72. package/addon/components/device/details.hbs +3 -1
  73. package/addon/components/device/form.hbs +4 -13
  74. package/addon/components/device/panel-header.hbs +3 -2
  75. package/addon/components/device/panel-header.js +1 -5
  76. package/addon/components/device/panel-tabs/vehicle.hbs +2 -2
  77. package/addon/components/device/pill.hbs +24 -16
  78. package/addon/components/device/summary.hbs +1 -0
  79. package/addon/components/device/telemetry.hbs +17 -0
  80. package/addon/components/device/telemetry.js +117 -0
  81. package/addon/components/device-event/details.hbs +6 -24
  82. package/addon/components/device-event/pill.hbs +24 -0
  83. package/addon/components/device-event/summary.hbs +1 -0
  84. package/addon/components/driver/details.hbs +9 -1
  85. package/addon/components/driver/form.hbs +6 -18
  86. package/addon/components/driver/panel-header.hbs +3 -4
  87. package/addon/components/driver/panel-header.js +8 -1
  88. package/addon/components/driver/pill.hbs +16 -8
  89. package/addon/components/driver/summary.hbs +1 -0
  90. package/addon/components/entity/pill.hbs +24 -0
  91. package/addon/components/entity/summary.hbs +1 -0
  92. package/addon/components/equipment/details.hbs +6 -2
  93. package/addon/components/equipment/form.hbs +10 -4
  94. package/addon/components/equipment/pill.hbs +24 -20
  95. package/addon/components/equipment/summary.hbs +1 -0
  96. package/addon/components/facilitator/pill.hbs +24 -0
  97. package/addon/components/facilitator/summary.hbs +1 -0
  98. package/addon/components/fleet/details.hbs +6 -2
  99. package/addon/components/fleet/driver-listing.hbs +3 -12
  100. package/addon/components/fleet/form.hbs +8 -4
  101. package/addon/components/fleet/pill.hbs +24 -0
  102. package/addon/components/fleet/summary.hbs +1 -0
  103. package/addon/components/fleet/vehicle-listing.hbs +3 -12
  104. package/addon/components/fleet-driver/pill.hbs +24 -0
  105. package/addon/components/fleet-driver/summary.hbs +1 -0
  106. package/addon/components/fleet-driver-listing.hbs +2 -1
  107. package/addon/components/fleet-panel/details.hbs +6 -2
  108. package/addon/components/fleet-panel/driver-listing.hbs +2 -1
  109. package/addon/components/fleet-panel/vehicle-listing.hbs +2 -1
  110. package/addon/components/fleet-vehicle-listing.hbs +2 -1
  111. package/addon/components/fuel-integration/form.hbs +125 -25
  112. package/addon/components/fuel-integration/form.js +74 -19
  113. package/addon/components/fuel-integration/hub.hbs +68 -65
  114. package/addon/components/fuel-integration/hub.js +15 -19
  115. package/addon/components/fuel-provider-connection/pill.hbs +24 -0
  116. package/addon/components/fuel-provider-connection/summary.hbs +1 -0
  117. package/addon/components/fuel-provider-transaction/pill.hbs +24 -0
  118. package/addon/components/fuel-provider-transaction/summary.hbs +1 -0
  119. package/addon/components/fuel-report/details.hbs +9 -3
  120. package/addon/components/fuel-report/form.hbs +6 -3
  121. package/addon/components/fuel-report/pill.hbs +24 -0
  122. package/addon/components/fuel-report/summary.hbs +1 -0
  123. package/addon/components/fuel-transaction-status.hbs +1 -0
  124. package/addon/components/fuel-transaction-status.js +8 -0
  125. package/addon/components/geofence-map.hbs +19 -0
  126. package/addon/components/geofence-map.js +83 -0
  127. package/addon/components/inspection-field/form.hbs +103 -0
  128. package/addon/components/inspection-field/form.js +211 -0
  129. package/addon/components/inspection-field/input.hbs +270 -0
  130. package/addon/components/inspection-field/input.js +472 -0
  131. package/addon/components/inspection-field/value.hbs +90 -0
  132. package/addon/components/inspection-field/value.js +158 -0
  133. package/addon/components/inspection-flyout.hbs +34 -0
  134. package/addon/components/inspection-flyout.js +66 -0
  135. package/addon/components/inspection-form/builder.hbs +88 -0
  136. package/addon/components/inspection-form/builder.js +211 -0
  137. package/addon/components/inspection-form/details/submissions.hbs +48 -0
  138. package/addon/components/inspection-form/details/submissions.js +42 -0
  139. package/addon/components/inspection-form/details.hbs +97 -0
  140. package/addon/components/inspection-form/details.js +57 -0
  141. package/addon/components/inspection-form/form.hbs +93 -0
  142. package/addon/components/inspection-form/form.js +79 -0
  143. package/addon/components/inspection-form/pill.hbs +24 -0
  144. package/addon/components/inspection-form/summary.hbs +1 -0
  145. package/addon/components/inspection-item-result/pill.hbs +24 -0
  146. package/addon/components/inspection-item-result/summary.hbs +1 -0
  147. package/addon/components/inspection-link/list.hbs +94 -0
  148. package/addon/components/inspection-link/list.js +129 -0
  149. package/addon/components/inspection-sheet/group.hbs +45 -0
  150. package/addon/components/inspection-sheet/group.js +75 -0
  151. package/addon/components/inspection-sheet.hbs +77 -0
  152. package/addon/components/inspection-sheet.js +152 -0
  153. package/addon/components/inspection-submission/details.hbs +149 -0
  154. package/addon/components/inspection-submission/details.js +130 -0
  155. package/addon/components/inspection-submission/form.hbs +114 -0
  156. package/addon/components/inspection-submission/form.js +129 -0
  157. package/addon/components/inspection-submission/photos.hbs +20 -0
  158. package/addon/components/inspection-submission/photos.js +57 -0
  159. package/addon/components/inspection-submission/pill.hbs +24 -0
  160. package/addon/components/inspection-submission/summary.hbs +1 -0
  161. package/addon/components/integrated-vendor/pill.hbs +24 -0
  162. package/addon/components/integrated-vendor/summary.hbs +1 -0
  163. package/addon/components/issue/details.hbs +12 -0
  164. package/addon/components/issue/details.js +36 -0
  165. package/addon/components/issue/form.hbs +10 -5
  166. package/addon/components/issue/pill.hbs +24 -0
  167. package/addon/components/issue/summary.hbs +1 -0
  168. package/addon/components/layout/fleet-ops-sidebar.js +14 -3
  169. package/addon/components/maintenance/details.hbs +9 -3
  170. package/addon/components/maintenance/form.hbs +22 -3
  171. package/addon/components/maintenance/form.js +4 -4
  172. package/addon/components/maintenance/panel-header.hbs +14 -12
  173. package/addon/components/maintenance/pill.hbs +24 -0
  174. package/addon/components/maintenance/summary.hbs +1 -0
  175. package/addon/components/maintenance-schedule/details.hbs +7 -3
  176. package/addon/components/maintenance-schedule/form.hbs +20 -2
  177. package/addon/components/maintenance-schedule/form.js +6 -6
  178. package/addon/components/maintenance-schedule/pill.hbs +24 -0
  179. package/addon/components/maintenance-schedule/summary.hbs +1 -0
  180. package/addon/components/maintenance-schedule/work-orders.hbs +39 -0
  181. package/addon/components/maintenance-schedule/work-orders.js +37 -0
  182. package/addon/components/maintenance-subject/pill.hbs +24 -0
  183. package/addon/components/maintenance-subject/summary.hbs +1 -0
  184. package/addon/components/manifest/pill.hbs +24 -0
  185. package/addon/components/manifest/summary.hbs +1 -0
  186. package/addon/components/manifest-stop/pill.hbs +24 -0
  187. package/addon/components/manifest-stop/summary.hbs +1 -0
  188. package/addon/components/map/drawer/device-event-listing.hbs +4 -14
  189. package/addon/components/map/drawer/device-event-listing.js +3 -1
  190. package/addon/components/map/drawer/driver-listing.js +6 -2
  191. package/addon/components/map/drawer/position-listing.hbs +2 -16
  192. package/addon/components/map/drawer/vehicle-listing.js +1 -2
  193. package/addon/components/modals/add-driver-shift.hbs +2 -1
  194. package/addon/components/modals/assign-driver.hbs +2 -16
  195. package/addon/components/modals/attach-device.hbs +2 -1
  196. package/addon/components/modals/attach-equipment.hbs +2 -4
  197. package/addon/components/modals/attach-telematic-device.hbs +8 -1
  198. package/addon/components/modals/attach-trailer.hbs +2 -1
  199. package/addon/components/modals/bulk-assign-driver.hbs +2 -9
  200. package/addon/components/modals/bulk-assign-orders.hbs +2 -9
  201. package/addon/components/modals/driver-assign-order.hbs +2 -6
  202. package/addon/components/modals/driver-assign-vehicle.hbs +4 -12
  203. package/addon/components/modals/driver-assign-vehicle.js +30 -0
  204. package/addon/components/modals/driver-assign-vendor.hbs +2 -1
  205. package/addon/components/modals/driver-form.hbs +4 -2
  206. package/addon/components/modals/fleet-assign-driver.hbs +2 -1
  207. package/addon/components/modals/fleet-assign-vehicle.hbs +2 -1
  208. package/addon/components/modals/fleet-form.hbs +4 -2
  209. package/addon/components/modals/fuel-connection-diagnostics.hbs +35 -0
  210. package/addon/components/modals/fuel-connection-diagnostics.js +87 -0
  211. package/addon/components/modals/fuel-report-form.hbs +4 -2
  212. package/addon/components/modals/fuel-transaction-action.hbs +48 -0
  213. package/addon/components/modals/fuel-transaction-action.js +98 -0
  214. package/addon/components/modals/group-form.hbs +2 -1
  215. package/addon/components/modals/inspection-follow-up.hbs +31 -0
  216. package/addon/components/modals/inspection-follow-up.js +39 -0
  217. package/addon/components/modals/inspection-link.hbs +100 -0
  218. package/addon/components/modals/inspection-link.js +83 -0
  219. package/addon/components/modals/issue-assign.hbs +2 -1
  220. package/addon/components/modals/issue-from.hbs +8 -4
  221. package/addon/components/modals/map-layer-form.hbs +2 -1
  222. package/addon/components/modals/order-assign-driver.hbs +2 -16
  223. package/addon/components/modals/order-form.hbs +8 -46
  224. package/addon/components/modals/order-route-form.hbs +8 -4
  225. package/addon/components/modals/place-assign-vendor.hbs +3 -2
  226. package/addon/components/modals/place-details.hbs +1 -1
  227. package/addon/components/modals/point-map.hbs +1 -1
  228. package/addon/components/modals/radar-notice.hbs +24 -0
  229. package/addon/components/modals/radar-notice.js +8 -0
  230. package/addon/components/modals/radar-save-view.hbs +7 -0
  231. package/addon/components/modals/radar-select-resource.hbs +35 -0
  232. package/addon/components/modals/radar-send-pin.hbs +20 -0
  233. package/addon/components/modals/radar-send-pin.js +8 -0
  234. package/addon/components/modals/select-trailer.hbs +2 -4
  235. package/addon/components/modals/vehicle-form.hbs +2 -1
  236. package/addon/components/modals/vendor-form.hbs +2 -3
  237. package/addon/components/order/details/activity.js +9 -0
  238. package/addon/components/order/details/detail.hbs +2 -32
  239. package/addon/components/order/form/details.hbs +10 -48
  240. package/addon/components/order/form/route.hbs +10 -12
  241. package/addon/components/order/form/service-rate.hbs +2 -3
  242. package/addon/components/order/pill.hbs +24 -34
  243. package/addon/components/order/route-editor.hbs +8 -4
  244. package/addon/components/order/schedule-card.hbs +2 -1
  245. package/addon/components/order/summary.hbs +1 -0
  246. package/addon/components/order-config/pill.hbs +24 -0
  247. package/addon/components/order-config/summary.hbs +1 -0
  248. package/addon/components/part/details.hbs +15 -3
  249. package/addon/components/part/details.js +37 -1
  250. package/addon/components/part/form.hbs +12 -5
  251. package/addon/components/part/pill.hbs +24 -0
  252. package/addon/components/part/summary.hbs +1 -0
  253. package/addon/components/payload/pill.hbs +24 -0
  254. package/addon/components/payload/summary.hbs +1 -0
  255. package/addon/components/place/details.hbs +1 -1
  256. package/addon/components/place/pill.hbs +24 -0
  257. package/addon/components/place/summary.hbs +1 -0
  258. package/addon/components/position/pill.hbs +24 -0
  259. package/addon/components/position/summary.hbs +1 -0
  260. package/addon/components/positions-replay.hbs +2 -4
  261. package/addon/components/public-inspection.hbs +174 -0
  262. package/addon/components/public-inspection.js +279 -0
  263. package/addon/components/purchase-rate/pill.hbs +24 -0
  264. package/addon/components/purchase-rate/summary.hbs +1 -0
  265. package/addon/components/radar/agenda/entry.hbs +17 -0
  266. package/addon/components/radar/agenda/entry.js +26 -0
  267. package/addon/components/radar/agenda/lane.hbs +33 -0
  268. package/addon/components/radar/agenda/lane.js +80 -0
  269. package/addon/components/radar/agenda.hbs +69 -0
  270. package/addon/components/radar/agenda.js +29 -0
  271. package/addon/components/radar/briefing.hbs +108 -0
  272. package/addon/components/radar/briefing.js +43 -0
  273. package/addon/components/radar/bulk-bar.hbs +41 -0
  274. package/addon/components/radar/bulk-bar.js +32 -0
  275. package/addon/components/radar/decision-card.hbs +28 -0
  276. package/addon/components/radar/decision-card.js +7 -0
  277. package/addon/components/radar/empty-state.hbs +30 -0
  278. package/addon/components/radar/empty-state.js +27 -0
  279. package/addon/components/radar/filter-pills.hbs +22 -0
  280. package/addon/components/radar/filter-pills.js +3 -0
  281. package/addon/components/radar/handover-card.hbs +62 -0
  282. package/addon/components/radar/handover-card.js +14 -0
  283. package/addon/components/radar/header.hbs +28 -0
  284. package/addon/components/radar/header.js +12 -0
  285. package/addon/components/radar/item-drawer.hbs +92 -0
  286. package/addon/components/radar/item-drawer.js +37 -0
  287. package/addon/components/radar/item-list.hbs +20 -0
  288. package/addon/components/radar/item-list.js +3 -0
  289. package/addon/components/radar/item-row.hbs +90 -0
  290. package/addon/components/radar/item-row.js +43 -0
  291. package/addon/components/radar/keyboard-hints.hbs +8 -0
  292. package/addon/components/radar/keyboard-hints.js +3 -0
  293. package/addon/components/radar/subject.hbs +14 -0
  294. package/addon/components/radar/subject.js +26 -0
  295. package/addon/components/radar/view-bar.hbs +27 -0
  296. package/addon/components/radar/view-bar.js +16 -0
  297. package/addon/components/recurring-order-schedule/pill.hbs +24 -0
  298. package/addon/components/recurring-order-schedule/summary.hbs +1 -0
  299. package/addon/components/resource-activity.hbs +1 -0
  300. package/addon/components/resource-activity.js +4 -0
  301. package/addon/components/route/pill.hbs +24 -0
  302. package/addon/components/route/summary.hbs +1 -0
  303. package/addon/components/select-option/asset-connection.hbs +12 -0
  304. package/addon/components/select-option/asset.hbs +12 -0
  305. package/addon/components/select-option/attachable.hbs +12 -0
  306. package/addon/components/select-option/contact.hbs +12 -0
  307. package/addon/components/select-option/customer.hbs +12 -0
  308. package/addon/components/select-option/device-event.hbs +12 -0
  309. package/addon/components/select-option/device.hbs +12 -0
  310. package/addon/components/select-option/driver.hbs +12 -0
  311. package/addon/components/select-option/entity.hbs +12 -0
  312. package/addon/components/select-option/equipment.hbs +12 -0
  313. package/addon/components/select-option/facilitator.hbs +12 -0
  314. package/addon/components/select-option/fleet-driver.hbs +12 -0
  315. package/addon/components/select-option/fleet.hbs +12 -0
  316. package/addon/components/select-option/fuel-provider-connection.hbs +12 -0
  317. package/addon/components/select-option/fuel-provider-transaction.hbs +12 -0
  318. package/addon/components/select-option/fuel-report.hbs +12 -0
  319. package/addon/components/select-option/inspection-form.hbs +12 -0
  320. package/addon/components/select-option/inspection-item-result.hbs +12 -0
  321. package/addon/components/select-option/inspection-submission.hbs +12 -0
  322. package/addon/components/select-option/integrated-vendor.hbs +12 -0
  323. package/addon/components/select-option/issue.hbs +12 -0
  324. package/addon/components/select-option/maintenance-schedule.hbs +12 -0
  325. package/addon/components/select-option/maintenance-subject.hbs +12 -0
  326. package/addon/components/select-option/maintenance.hbs +12 -0
  327. package/addon/components/select-option/manifest-stop.hbs +12 -0
  328. package/addon/components/select-option/manifest.hbs +12 -0
  329. package/addon/components/select-option/order-config.hbs +12 -0
  330. package/addon/components/select-option/order.hbs +12 -0
  331. package/addon/components/select-option/part.hbs +12 -0
  332. package/addon/components/select-option/payload.hbs +12 -0
  333. package/addon/components/select-option/place.hbs +12 -0
  334. package/addon/components/select-option/position.hbs +12 -0
  335. package/addon/components/select-option/purchase-rate.hbs +12 -0
  336. package/addon/components/select-option/recurring-order-schedule.hbs +12 -0
  337. package/addon/components/select-option/route.hbs +12 -0
  338. package/addon/components/select-option/sensor.hbs +12 -0
  339. package/addon/components/select-option/service-area.hbs +12 -0
  340. package/addon/components/select-option/service-quote-item.hbs +12 -0
  341. package/addon/components/select-option/service-quote.hbs +12 -0
  342. package/addon/components/select-option/service-rate-fee.hbs +12 -0
  343. package/addon/components/select-option/service-rate-parcel-fee.hbs +12 -0
  344. package/addon/components/select-option/service-rate.hbs +12 -0
  345. package/addon/components/select-option/telematic.hbs +12 -0
  346. package/addon/components/select-option/trackable.hbs +8 -0
  347. package/addon/components/select-option/tracking-number.hbs +12 -0
  348. package/addon/components/select-option/tracking-status.hbs +12 -0
  349. package/addon/components/select-option/trailer.hbs +12 -0
  350. package/addon/components/select-option/vehicle-device.hbs +12 -0
  351. package/addon/components/select-option/vehicle.hbs +12 -0
  352. package/addon/components/select-option/vendor.hbs +12 -0
  353. package/addon/components/select-option/warranty.hbs +12 -0
  354. package/addon/components/select-option/waypoint.hbs +12 -0
  355. package/addon/components/select-option/work-order.hbs +12 -0
  356. package/addon/components/select-option/zone.hbs +12 -0
  357. package/addon/components/sensor/details.hbs +6 -2
  358. package/addon/components/sensor/form.hbs +6 -17
  359. package/addon/components/sensor/panel-header.hbs +1 -1
  360. package/addon/components/sensor/pill.hbs +24 -0
  361. package/addon/components/sensor/summary.hbs +1 -0
  362. package/addon/components/service-area/details.hbs +1 -20
  363. package/addon/components/service-area/pill.hbs +24 -0
  364. package/addon/components/service-area/summary.hbs +1 -0
  365. package/addon/components/service-quote/pill.hbs +24 -0
  366. package/addon/components/service-quote/summary.hbs +1 -0
  367. package/addon/components/service-quote-item/pill.hbs +24 -0
  368. package/addon/components/service-quote-item/summary.hbs +1 -0
  369. package/addon/components/service-rate/details.hbs +12 -6
  370. package/addon/components/service-rate/form.hbs +10 -8
  371. package/addon/components/service-rate/pill.hbs +24 -0
  372. package/addon/components/service-rate/summary.hbs +1 -0
  373. package/addon/components/service-rate-fee/pill.hbs +24 -0
  374. package/addon/components/service-rate-fee/summary.hbs +1 -0
  375. package/addon/components/service-rate-parcel-fee/pill.hbs +24 -0
  376. package/addon/components/service-rate-parcel-fee/summary.hbs +1 -0
  377. package/addon/components/table/cell/fleet-ops-option.hbs +20 -0
  378. package/addon/components/telematic/details.hbs +4 -1
  379. package/addon/components/telematic/details.js +1 -0
  380. package/addon/components/telematic/form.hbs +6 -1
  381. package/addon/components/telematic/form.js +1 -0
  382. package/addon/components/telematic/pill.hbs +24 -0
  383. package/addon/components/telematic/settings.hbs +6 -1
  384. package/addon/components/telematic/settings.js +1 -0
  385. package/addon/components/telematic/summary.hbs +1 -0
  386. package/addon/components/telematic/telemetry-status.hbs +43 -0
  387. package/addon/components/telematic/telemetry-status.js +95 -0
  388. package/addon/components/tracking-number/pill.hbs +24 -0
  389. package/addon/components/tracking-number/summary.hbs +1 -0
  390. package/addon/components/tracking-status/pill.hbs +24 -0
  391. package/addon/components/tracking-status/summary.hbs +1 -0
  392. package/addon/components/trailer/card.hbs +2 -5
  393. package/addon/components/trailer/details.hbs +11 -5
  394. package/addon/components/trailer/details.js +4 -0
  395. package/addon/components/trailer/form.hbs +6 -3
  396. package/addon/components/trailer/pill.hbs +24 -21
  397. package/addon/components/trailer/summary.hbs +1 -0
  398. package/addon/components/vehicle/card.hbs +4 -8
  399. package/addon/components/vehicle/details/inspections.hbs +49 -0
  400. package/addon/components/vehicle/details/inspections.js +44 -0
  401. package/addon/components/vehicle/details/maintenance-history.hbs +1 -1
  402. package/addon/components/vehicle/details/schedules.hbs +2 -2
  403. package/addon/components/vehicle/details/work-orders.hbs +1 -1
  404. package/addon/components/vehicle/details.hbs +18 -1
  405. package/addon/components/vehicle/form.hbs +2 -1
  406. package/addon/components/vehicle/panel-header.hbs +3 -4
  407. package/addon/components/vehicle/panel-header.js +8 -1
  408. package/addon/components/vehicle/pill.hbs +15 -24
  409. package/addon/components/vehicle/summary.hbs +1 -0
  410. package/addon/components/vehicle-device/pill.hbs +24 -0
  411. package/addon/components/vehicle-device/summary.hbs +1 -0
  412. package/addon/components/vendor/form.hbs +2 -4
  413. package/addon/components/vendor/personnel-manager.hbs +2 -9
  414. package/addon/components/vendor/personnel-tab.hbs +1 -0
  415. package/addon/components/vendor/personnel-tab.js +4 -0
  416. package/addon/components/vendor/pill.hbs +24 -0
  417. package/addon/components/vendor/summary.hbs +1 -0
  418. package/addon/components/warranty/details.hbs +3 -1
  419. package/addon/components/warranty/form.hbs +2 -1
  420. package/addon/components/warranty/pill.hbs +24 -0
  421. package/addon/components/warranty/summary.hbs +1 -0
  422. package/addon/components/waypoint/pill.hbs +24 -0
  423. package/addon/components/waypoint/summary.hbs +1 -0
  424. package/addon/components/widget/radar.hbs +28 -0
  425. package/addon/components/widget/radar.js +44 -0
  426. package/addon/components/work-order/details.hbs +20 -4
  427. package/addon/components/work-order/details.js +64 -1
  428. package/addon/components/work-order/form.hbs +20 -2
  429. package/addon/components/work-order/form.js +10 -10
  430. package/addon/components/work-order/panel-header.hbs +15 -13
  431. package/addon/components/work-order/pill.hbs +24 -0
  432. package/addon/components/work-order/summary.hbs +1 -0
  433. package/addon/components/zone/details.hbs +2 -21
  434. package/addon/components/zone/pill.hbs +24 -0
  435. package/addon/components/zone/summary.hbs +1 -0
  436. package/addon/controllers/connectivity/devices/index/details/sensors.js +4 -0
  437. package/addon/controllers/connectivity/devices/index/details.js +1 -18
  438. package/addon/controllers/connectivity/events/index.js +1 -4
  439. package/addon/controllers/connectivity/fuel-providers/details/index.js +6 -0
  440. package/addon/controllers/connectivity/fuel-providers/details/matching.js +13 -0
  441. package/addon/controllers/connectivity/fuel-providers/details/sync.js +21 -10
  442. package/addon/controllers/connectivity/fuel-providers/details/transactions.js +5 -16
  443. package/addon/controllers/connectivity/fuel-providers/details.js +76 -13
  444. package/addon/controllers/connectivity/fuel-providers/index.js +22 -17
  445. package/addon/controllers/connectivity/sensors/index.js +1 -3
  446. package/addon/controllers/connectivity/telematics/details/events.js +8 -0
  447. package/addon/controllers/connectivity/telematics/details/sensors.js +3 -2
  448. package/addon/controllers/connectivity/telematics/index.js +3 -1
  449. package/addon/controllers/maintenance/inspection-forms/index/details.js +29 -0
  450. package/addon/controllers/maintenance/inspection-forms/index/edit.js +39 -0
  451. package/addon/controllers/maintenance/inspection-forms/index/new.js +48 -0
  452. package/addon/controllers/maintenance/inspection-forms/index.js +90 -0
  453. package/addon/controllers/maintenance/inspection-submissions/index/details.js +53 -0
  454. package/addon/controllers/maintenance/inspection-submissions/index/edit.js +39 -0
  455. package/addon/controllers/maintenance/inspection-submissions/index/new.js +48 -0
  456. package/addon/controllers/maintenance/inspection-submissions/index.js +142 -0
  457. package/addon/controllers/maintenance/maintenances/index.js +6 -2
  458. package/addon/controllers/maintenance/schedules/index/details.js +4 -86
  459. package/addon/controllers/maintenance/schedules/index.js +15 -0
  460. package/addon/controllers/maintenance/work-orders/index.js +19 -1
  461. package/addon/controllers/management/contacts/customers.js +6 -4
  462. package/addon/controllers/management/contacts/index.js +6 -3
  463. package/addon/controllers/management/drivers/index/details.js +1 -66
  464. package/addon/controllers/management/drivers/index.js +9 -34
  465. package/addon/controllers/management/fleets/index/details.js +2 -15
  466. package/addon/controllers/management/fleets/index.js +12 -20
  467. package/addon/controllers/management/fuel-reports/index.js +9 -59
  468. package/addon/controllers/management/fuel-transactions/index/details.js +51 -48
  469. package/addon/controllers/management/fuel-transactions/index.js +32 -32
  470. package/addon/controllers/management/index.js +1367 -49
  471. package/addon/controllers/management/issues/index/details.js +1 -33
  472. package/addon/controllers/management/issues/index.js +10 -59
  473. package/addon/controllers/management/trailers/index/details.js +4 -78
  474. package/addon/controllers/management/trailers/index.js +20 -22
  475. package/addon/controllers/management/vehicles/index/details.js +9 -77
  476. package/addon/controllers/management/vehicles/index.js +50 -72
  477. package/addon/controllers/management/vendors/index.js +6 -3
  478. package/addon/controllers/operations/orders/index.js +38 -11
  479. package/addon/controllers/operations/service-rates/index.js +10 -2
  480. package/addon/controllers/settings/payments/index.js +11 -2
  481. package/addon/extension.js +36 -0
  482. package/addon/helpers/initials.js +6 -0
  483. package/addon/helpers/leaflet-tile-url.js +6 -1
  484. package/addon/instance-initializers/register-fleetops-resource-descriptors.js +29 -0
  485. package/addon/modifiers/inspection-flyout.js +237 -0
  486. package/addon/modifiers/radar-keyboard.js +78 -0
  487. package/addon/modifiers/sync-value.js +26 -0
  488. package/addon/modifiers/when-changed.js +25 -0
  489. package/addon/routes/connectivity/fuel-providers/details/settings.js +5 -2
  490. package/addon/routes/connectivity/fuel-providers/details/sync.js +6 -13
  491. package/addon/routes/connectivity/fuel-providers/details/transactions.js +8 -7
  492. package/addon/routes/connectivity/fuel-providers/details.js +15 -2
  493. package/addon/routes/connectivity/fuel-providers/edit.js +2 -1
  494. package/addon/routes/connectivity/fuel-providers/index.js +5 -0
  495. package/addon/routes/maintenance/inspection-forms/index/details/index.js +3 -0
  496. package/addon/routes/maintenance/inspection-forms/index/details/submissions.js +3 -0
  497. package/addon/routes/maintenance/inspection-forms/index/details.js +18 -0
  498. package/addon/routes/maintenance/inspection-forms/index/edit.js +18 -0
  499. package/addon/routes/maintenance/inspection-forms/index/new.js +3 -0
  500. package/addon/routes/maintenance/inspection-forms/index.js +22 -0
  501. package/addon/routes/maintenance/inspection-forms.js +3 -0
  502. package/addon/routes/maintenance/inspection-submissions/index/details/audit.js +8 -0
  503. package/addon/routes/maintenance/inspection-submissions/index/details/index.js +3 -0
  504. package/addon/routes/maintenance/inspection-submissions/index/details/photos.js +8 -0
  505. package/addon/routes/maintenance/inspection-submissions/index/details.js +18 -0
  506. package/addon/routes/maintenance/inspection-submissions/index/edit.js +18 -0
  507. package/addon/routes/maintenance/inspection-submissions/index/new.js +3 -0
  508. package/addon/routes/maintenance/inspection-submissions/index.js +25 -0
  509. package/addon/routes/maintenance/inspection-submissions.js +3 -0
  510. package/addon/routes/management/fuel-transactions/index/details.js +12 -2
  511. package/addon/routes/management/fuel-transactions/index.js +6 -0
  512. package/addon/routes/management/index.js +17 -1
  513. package/addon/routes/management/vehicles/index/details/inspections.js +3 -0
  514. package/addon/routes/management/vehicles/index.js +2 -2
  515. package/addon/routes.js +24 -0
  516. package/addon/services/contact-actions.js +22 -4
  517. package/addon/services/customer-actions.js +7 -4
  518. package/addon/services/device-actions.js +28 -0
  519. package/addon/services/driver-actions.js +79 -16
  520. package/addon/services/equipment-actions.js +15 -6
  521. package/addon/services/fleet-actions.js +20 -4
  522. package/addon/services/fuel-report-actions.js +7 -7
  523. package/addon/services/inspection-form-actions.js +236 -0
  524. package/addon/services/inspection-submission-actions.js +273 -0
  525. package/addon/services/issue-actions.js +58 -7
  526. package/addon/services/maintenance-actions.js +22 -3
  527. package/addon/services/maintenance-schedule-actions.js +70 -5
  528. package/addon/services/movement-tracker.js +52 -1
  529. package/addon/services/part-actions.js +13 -7
  530. package/addon/services/place-actions.js +8 -7
  531. package/addon/services/service-area-actions.js +27 -4
  532. package/addon/services/trailer-actions.js +60 -7
  533. package/addon/services/vehicle-actions.js +58 -3
  534. package/addon/services/vendor-actions.js +8 -7
  535. package/addon/services/work-order-actions.js +14 -5
  536. package/addon/services/zone-actions.js +27 -4
  537. package/addon/styles/fleetops-engine.css +3621 -75
  538. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -2
  539. package/addon/templates/connectivity/events/details.hbs +2 -2
  540. package/addon/templates/connectivity/fuel-providers/details/index.hbs +65 -35
  541. package/addon/templates/connectivity/fuel-providers/details/matching.hbs +2 -2
  542. package/addon/templates/connectivity/fuel-providers/details/sync.hbs +54 -43
  543. package/addon/templates/connectivity/fuel-providers/details/transactions.hbs +10 -5
  544. package/addon/templates/connectivity/fuel-providers/details.hbs +38 -13
  545. package/addon/templates/connectivity/fuel-providers/edit.hbs +1 -0
  546. package/addon/templates/connectivity/fuel-providers/index.hbs +2 -1
  547. package/addon/templates/connectivity/fuel-providers/new.hbs +1 -1
  548. package/addon/templates/connectivity/telematics/details/attachments.hbs +2 -1
  549. package/addon/templates/maintenance/inspection-forms/index/details/index.hbs +1 -0
  550. package/addon/templates/maintenance/inspection-forms/index/details/submissions.hbs +2 -0
  551. package/addon/templates/maintenance/inspection-forms/index/details.hbs +14 -0
  552. package/addon/templates/maintenance/inspection-forms/index/edit.hbs +11 -0
  553. package/addon/templates/maintenance/inspection-forms/index/new.hbs +11 -0
  554. package/addon/templates/maintenance/inspection-forms/index.hbs +28 -0
  555. package/addon/templates/maintenance/inspection-forms.hbs +1 -0
  556. package/addon/templates/maintenance/inspection-submissions/index/details/audit.hbs +1 -0
  557. package/addon/templates/maintenance/inspection-submissions/index/details/index.hbs +1 -0
  558. package/addon/templates/maintenance/inspection-submissions/index/details/photos.hbs +1 -0
  559. package/addon/templates/maintenance/inspection-submissions/index/details.hbs +14 -0
  560. package/addon/templates/maintenance/inspection-submissions/index/edit.hbs +11 -0
  561. package/addon/templates/maintenance/inspection-submissions/index/new.hbs +11 -0
  562. package/addon/templates/maintenance/inspection-submissions/index.hbs +28 -0
  563. package/addon/templates/maintenance/inspection-submissions.hbs +1 -0
  564. package/addon/templates/management/fuel-transactions/index/details.hbs +64 -62
  565. package/addon/templates/management/fuel-transactions/index.hbs +9 -31
  566. package/addon/templates/management/index.hbs +110 -102
  567. package/addon/templates/management/vehicles/index/details/inspections.hbs +2 -0
  568. package/addon/templates/operations/orders/index.hbs +2 -4
  569. package/addon/utils/context-panel.js +34 -0
  570. package/addon/utils/device-table-columns.js +18 -27
  571. package/addon/utils/fleet-ops-options.js +40 -0
  572. package/addon/utils/fuel-integration-format.js +43 -0
  573. package/addon/utils/fuel-transaction.js +115 -0
  574. package/addon/utils/identity-cell-resource.js +56 -0
  575. package/addon/utils/inspection-answers.js +283 -0
  576. package/addon/utils/inspection-field-types.js +74 -0
  577. package/addon/utils/inspection-form-structure.js +161 -0
  578. package/addon/utils/radar.js +212 -0
  579. package/addon/utils/relation-value.js +37 -0
  580. package/addon/utils/resource-descriptors/assets.js +246 -0
  581. package/addon/utils/resource-descriptors/connectivity.js +146 -0
  582. package/addon/utils/resource-descriptors/fuel.js +75 -0
  583. package/addon/utils/resource-descriptors/helpers.js +191 -0
  584. package/addon/utils/resource-descriptors/index.js +35 -0
  585. package/addon/utils/resource-descriptors/maintenance.js +175 -0
  586. package/addon/utils/resource-descriptors/operations.js +215 -0
  587. package/addon/utils/resource-descriptors/people.js +173 -0
  588. package/addon/utils/resource-descriptors/polymorphic.js +123 -0
  589. package/addon/utils/resource-descriptors/sub-records.js +297 -0
  590. package/addon/utils/telemetry-timestamp.js +6 -0
  591. package/app/components/asset/pill.js +1 -0
  592. package/app/components/asset/summary.js +1 -0
  593. package/app/components/asset-connection/pill.js +1 -0
  594. package/app/components/asset-connection/summary.js +1 -0
  595. package/app/components/attachable/pill.js +1 -0
  596. package/app/components/attachable/summary.js +1 -0
  597. package/app/components/cell/asset-connection-identity.js +1 -0
  598. package/app/components/cell/{attached-vehicle.js → asset-identity.js} +1 -1
  599. package/app/components/cell/attachable-identity.js +1 -0
  600. package/app/components/cell/{telematic-device.js → contact-identity.js} +1 -1
  601. package/app/components/cell/customer-identity.js +1 -0
  602. package/app/components/cell/device-event-identity.js +1 -0
  603. package/app/components/cell/entity-identity.js +1 -0
  604. package/app/components/cell/facilitator-identity.js +1 -0
  605. package/app/components/cell/fleet-driver-identity.js +1 -0
  606. package/app/components/cell/fleet-identity.js +1 -0
  607. package/app/components/cell/fuel-provider-connection-identity.js +1 -0
  608. package/app/components/cell/fuel-provider-transaction-identity.js +1 -0
  609. package/app/components/cell/fuel-report-identity.js +1 -0
  610. package/app/components/cell/inspection-form-identity.js +1 -0
  611. package/app/components/cell/inspection-item-result-identity.js +1 -0
  612. package/app/components/cell/inspection-submission-identity.js +1 -0
  613. package/app/components/cell/integrated-vendor-identity.js +1 -0
  614. package/app/components/cell/issue-identity.js +1 -0
  615. package/app/components/cell/maintenance-identity.js +1 -0
  616. package/app/components/cell/maintenance-schedule-identity.js +1 -0
  617. package/app/components/cell/maintenance-subject-identity.js +1 -0
  618. package/app/components/cell/manifest-identity.js +1 -0
  619. package/app/components/cell/manifest-stop-identity.js +1 -0
  620. package/app/components/cell/order-config-identity.js +1 -0
  621. package/app/components/cell/order-identity.js +1 -0
  622. package/app/components/cell/payload-identity.js +1 -0
  623. package/app/components/cell/place-identity.js +1 -0
  624. package/app/components/cell/position-identity.js +1 -0
  625. package/app/components/cell/purchase-rate-identity.js +1 -0
  626. package/app/components/cell/recurring-order-schedule-identity.js +1 -0
  627. package/app/components/cell/resource-list.js +1 -0
  628. package/app/components/cell/route-identity.js +1 -0
  629. package/app/components/cell/sensor-identity.js +1 -0
  630. package/app/components/cell/service-area-identity.js +1 -0
  631. package/app/components/cell/service-quote-identity.js +1 -0
  632. package/app/components/cell/service-quote-item-identity.js +1 -0
  633. package/app/components/cell/service-rate-fee-identity.js +1 -0
  634. package/app/components/cell/service-rate-identity.js +1 -0
  635. package/app/components/cell/service-rate-parcel-fee-identity.js +1 -0
  636. package/app/components/cell/{telematic-provider.js → telematic-identity.js} +1 -1
  637. package/app/components/cell/tracking-number-identity.js +1 -0
  638. package/app/components/cell/tracking-status-identity.js +1 -0
  639. package/app/components/cell/trailer-identity.js +1 -0
  640. package/app/components/cell/vehicle-device-identity.js +1 -0
  641. package/app/components/cell/vendor-identity.js +1 -0
  642. package/app/components/cell/warranty-identity.js +1 -0
  643. package/app/components/cell/waypoint-identity.js +1 -0
  644. package/app/components/cell/work-order-identity.js +1 -0
  645. package/app/components/cell/zone-identity.js +1 -0
  646. package/app/components/{cell/driver-name.js → contact/pill.js} +1 -1
  647. package/app/components/{cell/vehicle-name.js → contact/summary.js} +1 -1
  648. package/app/components/customer/pill.js +1 -0
  649. package/app/components/customer/summary.js +1 -0
  650. package/app/components/device/summary.js +1 -0
  651. package/app/components/device/telemetry.js +1 -0
  652. package/app/components/device-event/pill.js +1 -0
  653. package/app/components/device-event/summary.js +1 -0
  654. package/app/components/driver/summary.js +1 -0
  655. package/app/components/entity/pill.js +1 -0
  656. package/app/components/entity/summary.js +1 -0
  657. package/app/components/equipment/card.js +1 -0
  658. package/app/components/equipment/panel-header.js +1 -0
  659. package/app/components/equipment/summary.js +1 -0
  660. package/app/components/{filter/multi-model.js → facilitator/pill.js} +1 -1
  661. package/app/components/facilitator/summary.js +1 -0
  662. package/app/components/fleet/pill.js +1 -0
  663. package/app/components/fleet/summary.js +1 -0
  664. package/app/components/fleet-driver/pill.js +1 -0
  665. package/app/components/fleet-driver/summary.js +1 -0
  666. package/app/components/fuel-provider-connection/pill.js +1 -0
  667. package/app/components/fuel-provider-connection/summary.js +1 -0
  668. package/app/components/fuel-provider-transaction/pill.js +1 -0
  669. package/app/components/fuel-provider-transaction/summary.js +1 -0
  670. package/app/components/fuel-report/pill.js +1 -0
  671. package/app/components/fuel-report/summary.js +1 -0
  672. package/app/components/fuel-transaction-status.js +1 -0
  673. package/app/components/geofence-map.js +1 -0
  674. package/app/components/inspection-field/form.js +1 -0
  675. package/app/components/inspection-field/input.js +1 -0
  676. package/app/components/inspection-field/value.js +1 -0
  677. package/app/components/inspection-flyout.js +1 -0
  678. package/app/components/inspection-form/builder.js +1 -0
  679. package/app/components/inspection-form/details/submissions.js +1 -0
  680. package/app/components/inspection-form/details.js +1 -0
  681. package/app/components/inspection-form/form.js +1 -0
  682. package/app/components/inspection-form/pill.js +1 -0
  683. package/app/components/inspection-form/summary.js +1 -0
  684. package/app/components/inspection-item-result/pill.js +1 -0
  685. package/app/components/inspection-item-result/summary.js +1 -0
  686. package/app/components/inspection-link/list.js +1 -0
  687. package/app/components/inspection-sheet/group.js +1 -0
  688. package/app/components/inspection-sheet.js +1 -0
  689. package/app/components/inspection-submission/details.js +1 -0
  690. package/app/components/inspection-submission/form.js +1 -0
  691. package/app/components/inspection-submission/photos.js +1 -0
  692. package/app/components/inspection-submission/pill.js +1 -0
  693. package/app/components/inspection-submission/summary.js +1 -0
  694. package/app/components/integrated-vendor/pill.js +1 -0
  695. package/app/components/integrated-vendor/summary.js +1 -0
  696. package/app/components/issue/pill.js +1 -0
  697. package/app/components/issue/summary.js +1 -0
  698. package/app/components/issue/timeline.js +1 -0
  699. package/app/components/layout/fleet-ops-sidebar/operations-monitor.js +1 -0
  700. package/app/components/maintenance/pill.js +1 -0
  701. package/app/components/maintenance/summary.js +1 -0
  702. package/app/components/maintenance-schedule/pill.js +1 -0
  703. package/app/components/maintenance-schedule/summary.js +1 -0
  704. package/app/components/maintenance-schedule/work-orders.js +1 -0
  705. package/app/components/maintenance-subject/pill.js +1 -0
  706. package/app/components/maintenance-subject/summary.js +1 -0
  707. package/app/components/manifest/pill.js +1 -0
  708. package/app/components/manifest/summary.js +1 -0
  709. package/app/components/manifest-stop/pill.js +1 -0
  710. package/app/components/manifest-stop/summary.js +1 -0
  711. package/app/components/modals/fuel-connection-diagnostics.js +1 -0
  712. package/app/components/modals/fuel-transaction-action.js +1 -0
  713. package/app/components/modals/inspection-follow-up.js +1 -0
  714. package/app/components/modals/inspection-link.js +1 -0
  715. package/app/components/modals/radar-notice.js +1 -0
  716. package/app/components/modals/radar-save-view.js +1 -0
  717. package/app/components/modals/radar-select-resource.js +1 -0
  718. package/app/components/modals/radar-send-pin.js +1 -0
  719. package/app/components/order/summary.js +1 -0
  720. package/app/components/order-config/pill.js +1 -0
  721. package/app/components/order-config/summary.js +1 -0
  722. package/app/components/part/card.js +1 -0
  723. package/app/components/part/panel-header.js +1 -0
  724. package/app/components/part/pill.js +1 -0
  725. package/app/components/part/summary.js +1 -0
  726. package/app/components/payload/pill.js +1 -0
  727. package/app/components/payload/summary.js +1 -0
  728. package/app/components/place/pill.js +1 -0
  729. package/app/components/place/summary.js +1 -0
  730. package/app/components/position/pill.js +1 -0
  731. package/app/components/position/summary.js +1 -0
  732. package/app/components/public-inspection.js +1 -0
  733. package/app/components/purchase-rate/pill.js +1 -0
  734. package/app/components/purchase-rate/summary.js +1 -0
  735. package/app/components/radar/agenda/entry.js +1 -0
  736. package/app/components/radar/agenda/lane.js +1 -0
  737. package/app/components/radar/agenda.js +1 -0
  738. package/app/components/radar/briefing.js +1 -0
  739. package/app/components/radar/bulk-bar.js +1 -0
  740. package/app/components/radar/decision-card.js +1 -0
  741. package/app/components/radar/empty-state.js +1 -0
  742. package/app/components/radar/filter-pills.js +1 -0
  743. package/app/components/radar/handover-card.js +1 -0
  744. package/app/components/radar/header.js +1 -0
  745. package/app/components/radar/item-drawer.js +1 -0
  746. package/app/components/radar/item-list.js +1 -0
  747. package/app/components/radar/item-row.js +1 -0
  748. package/app/components/radar/keyboard-hints.js +1 -0
  749. package/app/components/radar/subject.js +1 -0
  750. package/app/components/radar/view-bar.js +1 -0
  751. package/app/components/recurring-order-schedule/pill.js +1 -0
  752. package/app/components/recurring-order-schedule/summary.js +1 -0
  753. package/app/components/resource-activity.js +1 -0
  754. package/app/components/route/pill.js +1 -0
  755. package/app/components/route/summary.js +1 -0
  756. package/app/components/select-option/asset-connection.js +1 -0
  757. package/app/components/select-option/asset.js +1 -0
  758. package/app/components/select-option/attachable.js +1 -0
  759. package/app/components/select-option/contact.js +1 -0
  760. package/app/components/select-option/customer.js +1 -0
  761. package/app/components/select-option/device-event.js +1 -0
  762. package/app/components/select-option/device.js +1 -0
  763. package/app/components/select-option/driver.js +1 -0
  764. package/app/components/select-option/entity.js +1 -0
  765. package/app/components/select-option/equipment.js +1 -0
  766. package/app/components/select-option/facilitator.js +1 -0
  767. package/app/components/select-option/fleet-driver.js +1 -0
  768. package/app/components/select-option/fleet.js +1 -0
  769. package/app/components/select-option/fuel-provider-connection.js +1 -0
  770. package/app/components/select-option/fuel-provider-transaction.js +1 -0
  771. package/app/components/select-option/fuel-report.js +1 -0
  772. package/app/components/select-option/inspection-form.js +1 -0
  773. package/app/components/select-option/inspection-item-result.js +1 -0
  774. package/app/components/select-option/inspection-submission.js +1 -0
  775. package/app/components/select-option/integrated-vendor.js +1 -0
  776. package/app/components/select-option/issue.js +1 -0
  777. package/app/components/select-option/maintenance-schedule.js +1 -0
  778. package/app/components/select-option/maintenance-subject.js +1 -0
  779. package/app/components/select-option/maintenance.js +1 -0
  780. package/app/components/select-option/manifest-stop.js +1 -0
  781. package/app/components/select-option/manifest.js +1 -0
  782. package/app/components/select-option/order-config.js +1 -0
  783. package/app/components/select-option/order.js +1 -0
  784. package/app/components/select-option/part.js +1 -0
  785. package/app/components/select-option/payload.js +1 -0
  786. package/app/components/select-option/place.js +1 -0
  787. package/app/components/select-option/position.js +1 -0
  788. package/app/components/select-option/purchase-rate.js +1 -0
  789. package/app/components/select-option/recurring-order-schedule.js +1 -0
  790. package/app/components/select-option/route.js +1 -0
  791. package/app/components/select-option/sensor.js +1 -0
  792. package/app/components/select-option/service-area.js +1 -0
  793. package/app/components/select-option/service-quote-item.js +1 -0
  794. package/app/components/select-option/service-quote.js +1 -0
  795. package/app/components/select-option/service-rate-fee.js +1 -0
  796. package/app/components/select-option/service-rate-parcel-fee.js +1 -0
  797. package/app/components/select-option/service-rate.js +1 -0
  798. package/app/components/select-option/telematic.js +1 -0
  799. package/app/components/select-option/trackable.js +1 -0
  800. package/app/components/select-option/tracking-number.js +1 -0
  801. package/app/components/select-option/tracking-status.js +1 -0
  802. package/app/components/select-option/trailer.js +1 -0
  803. package/app/components/select-option/vehicle-device.js +1 -0
  804. package/app/components/select-option/vehicle.js +1 -0
  805. package/app/components/select-option/vendor.js +1 -0
  806. package/app/components/select-option/warranty.js +1 -0
  807. package/app/components/select-option/waypoint.js +1 -0
  808. package/app/components/select-option/work-order.js +1 -0
  809. package/app/components/select-option/zone.js +1 -0
  810. package/app/components/sensor/pill.js +1 -0
  811. package/app/components/sensor/summary.js +1 -0
  812. package/app/components/service-area/pill.js +1 -0
  813. package/app/components/service-area/summary.js +1 -0
  814. package/app/components/service-quote/pill.js +1 -0
  815. package/app/components/service-quote/summary.js +1 -0
  816. package/app/components/service-quote-item/pill.js +1 -0
  817. package/app/components/service-quote-item/summary.js +1 -0
  818. package/app/components/service-rate/pill.js +1 -0
  819. package/app/components/service-rate/summary.js +1 -0
  820. package/app/components/service-rate-fee/pill.js +1 -0
  821. package/app/components/service-rate-fee/summary.js +1 -0
  822. package/app/components/service-rate-parcel-fee/pill.js +1 -0
  823. package/app/components/service-rate-parcel-fee/summary.js +1 -0
  824. package/app/components/table/cell/fleet-ops-option.js +1 -0
  825. package/app/components/telematic/pill.js +1 -0
  826. package/app/components/telematic/summary.js +1 -0
  827. package/app/components/telematic/telemetry-status.js +1 -0
  828. package/app/components/tracking-number/pill.js +1 -0
  829. package/app/components/tracking-number/summary.js +1 -0
  830. package/app/components/tracking-status/pill.js +1 -0
  831. package/app/components/tracking-status/summary.js +1 -0
  832. package/app/components/trailer/summary.js +1 -0
  833. package/app/components/vehicle/details/inspections.js +1 -0
  834. package/app/components/vehicle/summary.js +1 -0
  835. package/app/components/vehicle-device/pill.js +1 -0
  836. package/app/components/vehicle-device/summary.js +1 -0
  837. package/app/components/vendor/personnel-tab.js +1 -0
  838. package/app/components/vendor/pill.js +1 -0
  839. package/app/components/vendor/summary.js +1 -0
  840. package/app/components/warranty/pill.js +1 -0
  841. package/app/components/warranty/summary.js +1 -0
  842. package/app/components/waypoint/pill.js +1 -0
  843. package/app/components/waypoint/summary.js +1 -0
  844. package/app/components/widget/radar.js +1 -0
  845. package/app/components/work-order/panel-header.js +1 -0
  846. package/app/components/work-order/pill.js +1 -0
  847. package/app/components/work-order/summary.js +1 -0
  848. package/app/components/zone/pill.js +1 -0
  849. package/app/components/zone/summary.js +1 -0
  850. package/app/controllers/maintenance/inspection-forms/index/details.js +1 -0
  851. package/app/controllers/maintenance/inspection-forms/index/edit.js +1 -0
  852. package/app/controllers/maintenance/inspection-forms/index/new.js +1 -0
  853. package/app/controllers/maintenance/inspection-forms/index.js +1 -0
  854. package/app/controllers/maintenance/inspection-submissions/index/details.js +1 -0
  855. package/app/controllers/maintenance/inspection-submissions/index/edit.js +1 -0
  856. package/app/controllers/maintenance/inspection-submissions/index/new.js +1 -0
  857. package/app/controllers/maintenance/inspection-submissions/index.js +1 -0
  858. package/app/helpers/initials.js +1 -0
  859. package/app/modifiers/inspection-flyout.js +1 -0
  860. package/app/modifiers/radar-keyboard.js +1 -0
  861. package/app/modifiers/sync-value.js +1 -0
  862. package/app/modifiers/when-changed.js +1 -0
  863. package/app/routes/maintenance/inspection-forms/index/details/index.js +1 -0
  864. package/app/routes/maintenance/inspection-forms/index/details/submissions.js +1 -0
  865. package/app/routes/maintenance/inspection-forms/index/details.js +1 -0
  866. package/app/routes/maintenance/inspection-forms/index/edit.js +1 -0
  867. package/app/routes/maintenance/inspection-forms/index/new.js +1 -0
  868. package/app/routes/maintenance/inspection-forms/index.js +1 -0
  869. package/app/routes/maintenance/inspection-forms.js +1 -0
  870. package/app/routes/maintenance/inspection-submissions/index/details/audit.js +1 -0
  871. package/app/routes/maintenance/inspection-submissions/index/details/index.js +1 -0
  872. package/app/routes/maintenance/inspection-submissions/index/details/photos.js +1 -0
  873. package/app/routes/maintenance/inspection-submissions/index/details.js +1 -0
  874. package/app/routes/maintenance/inspection-submissions/index/edit.js +1 -0
  875. package/app/routes/maintenance/inspection-submissions/index/new.js +1 -0
  876. package/app/routes/maintenance/inspection-submissions/index.js +1 -0
  877. package/app/routes/maintenance/inspection-submissions.js +1 -0
  878. package/app/routes/management/vehicles/index/details/inspections.js +1 -0
  879. package/app/services/inspection-form-actions.js +1 -0
  880. package/app/services/inspection-submission-actions.js +1 -0
  881. package/app/templates/maintenance/inspection-forms/index/details/index.js +1 -0
  882. package/app/templates/maintenance/inspection-forms/index/details/submissions.js +1 -0
  883. package/app/templates/maintenance/inspection-forms/index/details.js +1 -0
  884. package/app/templates/maintenance/inspection-forms/index/edit.js +1 -0
  885. package/app/templates/maintenance/inspection-forms/index/new.js +1 -0
  886. package/app/templates/maintenance/inspection-forms/index.js +1 -0
  887. package/app/templates/maintenance/inspection-forms.js +1 -0
  888. package/app/templates/maintenance/inspection-submissions/index/details/audit.js +1 -0
  889. package/app/templates/maintenance/inspection-submissions/index/details/index.js +1 -0
  890. package/app/templates/maintenance/inspection-submissions/index/details/photos.js +1 -0
  891. package/app/templates/maintenance/inspection-submissions/index/details.js +1 -0
  892. package/app/templates/maintenance/inspection-submissions/index/edit.js +1 -0
  893. package/app/templates/maintenance/inspection-submissions/index/new.js +1 -0
  894. package/app/templates/maintenance/inspection-submissions/index.js +1 -0
  895. package/app/templates/maintenance/inspection-submissions.js +1 -0
  896. package/app/templates/management/vehicles/index/details/inspections.js +1 -0
  897. package/app/utils/inspection-field-types.js +2 -0
  898. package/app/utils/inspection-form-structure.js +1 -0
  899. package/app/utils/radar.js +1 -0
  900. package/composer.json +1 -1
  901. package/docs/AFAQY.md +91 -0
  902. package/docs/TELEMETRY_ARCHITECTURE.md +115 -0
  903. package/extension.json +1 -1
  904. package/package.json +3 -3
  905. package/scripts/coverage-summary.php +11 -0
  906. package/scripts/pest-bootstrap.php +1 -1
  907. package/server/config/afaqy.php +8 -0
  908. package/server/config/fuel-providers.php +28 -7
  909. package/server/config/telematics.php +8 -1
  910. package/server/config/telemetry.php +17 -0
  911. package/server/migrations/2026_09_09_000001_create_inspection_tables.php +152 -0
  912. package/server/migrations/2026_09_10_000001_convert_inspection_form_items_to_field_groups.php +44 -0
  913. package/server/migrations/2026_09_10_000002_add_token_to_inspection_links.php +39 -0
  914. package/server/migrations/2026_09_11_000001_add_assignee_and_pin_to_inspection_links.php +39 -0
  915. package/server/migrations/2026_09_15_000001_create_telematic_telemetry_tables.php +62 -0
  916. package/server/migrations/2026_09_15_000002_scope_fuel_transactions_to_connection.php +25 -0
  917. package/server/migrations/2026_09_16_000001_add_public_id_to_alerts_table.php +45 -0
  918. package/server/resources/views/mail/inspection-link-pin.blade.php +34 -0
  919. package/server/src/Auth/Schemas/FleetOps.php +12 -0
  920. package/server/src/Console/Commands/DrainTelematicInbox.php +82 -0
  921. package/server/src/Console/Commands/ProcessMaintenanceTriggers.php +4 -1
  922. package/server/src/Console/Commands/SyncTelematics.php +32 -2
  923. package/server/src/Contracts/TelemetryProviderInterface.php +27 -0
  924. package/server/src/Events/DeviceTelemetryUpdated.php +37 -0
  925. package/server/src/Events/VehicleLocationChanged.php +8 -4
  926. package/server/src/Exports/InspectionExport.php +125 -0
  927. package/server/src/Http/Controllers/Api/v1/GeofenceController.php +1 -1
  928. package/server/src/Http/Controllers/Api/v1/InspectionController.php +300 -0
  929. package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +40 -8
  930. package/server/src/Http/Controllers/Internal/v1/HubController.php +36 -11
  931. package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +326 -0
  932. package/server/src/Http/Controllers/Internal/v1/InspectionSubmissionController.php +229 -0
  933. package/server/src/Http/Controllers/Internal/v1/RadarController.php +1117 -0
  934. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +67 -0
  935. package/server/src/Http/Controllers/Public/PublicInspectionController.php +243 -0
  936. package/server/src/Http/Controllers/TelematicPositionWebhookController.php +60 -0
  937. package/server/src/Http/Controllers/TelematicWebhookController.php +3 -0
  938. package/server/src/Http/Filter/FuelProviderConnectionFilter.php +1 -1
  939. package/server/src/Http/Middleware/ForceJsonResponse.php +25 -0
  940. package/server/src/Http/Resources/v1/InspectionForm.php +153 -0
  941. package/server/src/Http/Resources/v1/InspectionItemResult.php +77 -0
  942. package/server/src/Http/Resources/v1/InspectionLink.php +84 -0
  943. package/server/src/Http/Resources/v1/InspectionSubmission.php +173 -0
  944. package/server/src/Jobs/PollTelematicTelemetry.php +118 -0
  945. package/server/src/Jobs/ProcessTelematicDelivery.php +122 -0
  946. package/server/src/Jobs/SyncFuelProviderTransactionsJob.php +21 -4
  947. package/server/src/Mail/InspectionLinkPinMail.php +57 -0
  948. package/server/src/Models/Driver.php +8 -2
  949. package/server/src/Models/FuelProviderTransaction.php +1 -0
  950. package/server/src/Models/InspectionForm.php +240 -0
  951. package/server/src/Models/InspectionItemResult.php +90 -0
  952. package/server/src/Models/InspectionLink.php +284 -0
  953. package/server/src/Models/InspectionSubmission.php +440 -0
  954. package/server/src/Models/Part.php +9 -2
  955. package/server/src/Models/WorkOrder.php +3 -0
  956. package/server/src/Observers/WorkOrderObserver.php +1 -1
  957. package/server/src/Providers/FleetOpsServiceProvider.php +9 -0
  958. package/server/src/Rules/Base64OrUrl.php +54 -0
  959. package/server/src/Support/FuelProviders/FuelProviderService.php +64 -27
  960. package/server/src/Support/FuelProviders/Providers/PetroAppFuelProvider.php +81 -15
  961. package/server/src/Support/InspectionFileStore.php +268 -0
  962. package/server/src/Support/InspectionFormSync.php +231 -0
  963. package/server/src/Support/InspectionLinkPin.php +150 -0
  964. package/server/src/Support/InspectionSubmitter.php +297 -0
  965. package/server/src/Support/Radar/RadarAgenda.php +375 -0
  966. package/server/src/Support/Radar/RadarBriefing.php +504 -0
  967. package/server/src/Support/Radar/RadarItemState.php +435 -0
  968. package/server/src/Support/Radar/RadarRules.php +1424 -0
  969. package/server/src/Support/Reporting/FleetOpsReportSchema.php +178 -0
  970. package/server/src/Support/Telematics/Afaqy/Payload.php +49 -0
  971. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +179 -61
  972. package/server/src/Support/Telematics/TelematicService.php +37 -6
  973. package/server/src/Support/Telematics/Telemetry/Configuration.php +30 -0
  974. package/server/src/Support/Telematics/Telemetry/Inbox.php +61 -0
  975. package/server/src/Support/Telematics/Telemetry/Ingestor.php +112 -0
  976. package/server/src/Support/Telematics/Telemetry/Queue.php +28 -0
  977. package/server/src/Support/Telematics/Telemetry/Sample.php +54 -0
  978. package/server/src/routes.php +76 -0
  979. package/server/tests/ApiManifestControllerContractsTest.php +9 -9
  980. package/server/tests/ControllerFilterContractsTest.php +1 -1
  981. package/server/tests/EventContractsTest.php +2 -1
  982. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +792 -0
  983. package/server/tests/Feature/Http/Api/GeofenceControllerContractsTest.php +4 -4
  984. package/server/tests/Feature/Http/Api/TrailerControllerLifecycleTest.php +1 -1
  985. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +1 -1
  986. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +1 -1
  987. package/server/tests/Feature/Http/Internal/HubControllerTest.php +28 -2
  988. package/server/tests/Feature/Http/Internal/OrderControllerCreateRecordTest.php +1 -1
  989. package/server/tests/Feature/Http/Internal/RadarControllerDatabaseTest.php +415 -0
  990. package/server/tests/Feature/Http/Internal/RadarControllerTest.php +594 -0
  991. package/server/tests/FuelProviderConnectionControllerContractsTest.php +83 -3
  992. package/server/tests/FuelProviderImplementationTest.php +10 -2
  993. package/server/tests/HubAndGettingStartedContractsTest.php +26 -3
  994. package/server/tests/InspectionControllerContractsTest.php +1329 -0
  995. package/server/tests/InspectionFieldContractsTest.php +1019 -0
  996. package/server/tests/InspectionModelContractsTest.php +728 -0
  997. package/server/tests/InspectionSubmitterRulesTest.php +47 -0
  998. package/server/tests/ObserverContractsTest.php +1 -1
  999. package/server/tests/QueueJobContractsTest.php +3 -4
  1000. package/server/tests/RadarRoutesTest.php +35 -0
  1001. package/server/tests/ReportSchemaContractsTest.php +15 -1
  1002. package/server/tests/RouteRegistrationExecutionTest.php +109 -18
  1003. package/server/tests/Support/AfaqyTestCrypto.php +27 -0
  1004. package/server/tests/Support/ExampleTelemetryProvider.php +82 -0
  1005. package/server/tests/Support/TelemetryTestEnvironment.php +53 -0
  1006. package/server/tests/TelematicsHardeningTest.php +17 -17
  1007. package/server/tests/TrailerSupportCoverageTest.php +1 -1
  1008. package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +3 -3
  1009. package/server/tests/Unit/Support/FuelProviderServiceMatchingTest.php +127 -0
  1010. package/server/tests/Unit/Support/PetroAppFuelProviderTest.php +126 -1
  1011. package/server/tests/Unit/Support/RadarAgendaTest.php +200 -0
  1012. package/server/tests/Unit/Support/RadarBriefingTest.php +214 -0
  1013. package/server/tests/Unit/Support/RadarItemStateTest.php +260 -0
  1014. package/server/tests/Unit/Support/RadarRulesTest.php +523 -0
  1015. package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTest.php +9 -1
  1016. package/server/tests/Unit/Support/Telematics/Providers/AfaqyProviderTransportTest.php +8 -0
  1017. package/server/tests/Unit/Support/Telematics/Providers/AfaqyRealtimeContractTest.php +188 -0
  1018. package/server/tests/Unit/Support/Telematics/TelematicServiceTelemetryTest.php +1 -0
  1019. package/translations/ar-ae.yml +3 -1
  1020. package/translations/bg-bg.yaml +3 -1
  1021. package/translations/en-us.yaml +738 -15
  1022. package/translations/es-pa.yaml +3 -1
  1023. package/translations/fr-fr.yaml +3 -1
  1024. package/translations/mn-mn.yaml +3 -1
  1025. package/translations/pt-br.yaml +3 -1
  1026. package/translations/ru-ru.yaml +3 -1
  1027. package/translations/vi-vn.yaml +3 -1
  1028. package/addon/components/cell/attached-vehicle.hbs +0 -10
  1029. package/addon/components/cell/attached-vehicle.js +0 -58
  1030. package/addon/components/cell/device-identity.js +0 -129
  1031. package/addon/components/cell/driver-identity.js +0 -143
  1032. package/addon/components/cell/driver-name.hbs +0 -26
  1033. package/addon/components/cell/driver-name.js +0 -30
  1034. package/addon/components/cell/equipment-identity.js +0 -48
  1035. package/addon/components/cell/part-identity.js +0 -63
  1036. package/addon/components/cell/telematic-device.hbs +0 -38
  1037. package/addon/components/cell/telematic-device.js +0 -44
  1038. package/addon/components/cell/telematic-provider.hbs +0 -33
  1039. package/addon/components/cell/telematic-provider.js +0 -78
  1040. package/addon/components/cell/vehicle-identity.js +0 -138
  1041. package/addon/components/cell/vehicle-name.hbs +0 -23
  1042. package/addon/components/cell/vehicle-name.js +0 -3
  1043. package/addon/components/device/pill.js +0 -3
  1044. package/addon/components/driver/pill.js +0 -7
  1045. package/addon/components/equipment/pill.js +0 -3
  1046. package/addon/components/filter/multi-model.hbs +0 -19
  1047. package/addon/components/filter/multi-model.js +0 -90
  1048. package/addon/components/order/pill.js +0 -3
  1049. package/addon/components/trailer/pill.js +0 -3
  1050. package/addon/components/vehicle/pill.js +0 -7
  1051. package/addon/templates/connectivity/fuel-providers/details/settings.hbs +0 -11
@@ -0,0 +1,1329 @@
1
+ <?php
2
+
3
+ use Fleetbase\FleetOps\Http\Controllers\Api\v1\InspectionController;
4
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\InspectionFormController;
5
+ use Fleetbase\FleetOps\Http\Controllers\Internal\v1\InspectionSubmissionController;
6
+ use Fleetbase\FleetOps\Http\Controllers\Public\PublicInspectionController;
7
+ use Fleetbase\FleetOps\Http\Resources\v1\InspectionForm as InspectionFormResource;
8
+ use Fleetbase\FleetOps\Models\InspectionForm;
9
+ use Fleetbase\FleetOps\Models\InspectionItemResult;
10
+ use Fleetbase\FleetOps\Models\InspectionLink;
11
+ use Fleetbase\FleetOps\Models\InspectionSubmission;
12
+ use Fleetbase\FleetOps\Models\Issue;
13
+ use Fleetbase\FleetOps\Models\Vehicle;
14
+ use Fleetbase\FleetOps\Models\WorkOrder;
15
+ use Fleetbase\FleetOps\Support\InspectionFormSync;
16
+ use Illuminate\Config\Repository;
17
+ use Illuminate\Database\ConnectionResolver;
18
+ use Illuminate\Database\Eloquent\Model as EloquentModel;
19
+ use Illuminate\Database\Eloquent\ModelNotFoundException;
20
+ use Illuminate\Database\SQLiteConnection;
21
+ use Illuminate\Events\Dispatcher;
22
+ use Illuminate\Http\Exceptions\HttpResponseException;
23
+ use Illuminate\Http\JsonResponse;
24
+ use Illuminate\Http\Request;
25
+ use Illuminate\Support\Carbon;
26
+
27
+ if (!function_exists('Fleetbase\\Support\\auth')) {
28
+ eval('namespace Fleetbase\\Support; function auth() { return new class { public function user() { return null; } public function id() { return null; } }; }');
29
+ }
30
+
31
+ /*
32
+ * `abort()` is a foundation helper, absent locally. The public controller
33
+ * aborts with a ready-made JSON response, which Laravel raises as an
34
+ * HttpResponseException; the stub does the same so a refusal can be read
35
+ * back as the response it carries.
36
+ */
37
+ if (!function_exists('Fleetbase\\FleetOps\\Http\\Controllers\\Public\\abort')) {
38
+ eval('namespace Fleetbase\\FleetOps\\Http\\Controllers\\Public; function abort($code, $message = "", array $headers = []) { if ($code instanceof \\Symfony\\Component\\HttpFoundation\\Response) { throw new \\Illuminate\\Http\\Exceptions\\HttpResponseException($code); } throw new \\Symfony\\Component\\HttpKernel\\Exception\\HttpException($code, $message, null, $headers); }');
39
+ }
40
+
41
+ /**
42
+ * The same in-memory database the model contracts use. Every column is a
43
+ * nullable string; the controllers are what is under test here.
44
+ */
45
+ function fleetOpsInspectionControllerDatabase(): SQLiteConnection
46
+ {
47
+ $pdo = new PDO('sqlite::memory:');
48
+ // MySQL answers with a 4-byte SRID followed by the geometry's WKB, which is
49
+ // what the spatial trait parses when a row is read back. Handing back the
50
+ // WKT instead left a stored point unreadable ("Bad endian byte value").
51
+ $asStoredPoint = function ($wkt, $srid = 0, $axisOrder = null) {
52
+ if (!preg_match('/POINT\s*\(\s*(-?[\d.]+)\s+(-?[\d.]+)\s*\)/i', (string) $wkt, $pair)) {
53
+ return $wkt;
54
+ }
55
+
56
+ // WKB: little-endian marker, geometry type 1 (point), then x (lng) and y (lat).
57
+ return pack('V', (int) $srid) . pack('C', 1) . pack('V', 1) . pack('d', (float) $pair[1]) . pack('d', (float) $pair[2]);
58
+ };
59
+ $pdo->sqliteCreateFunction('ST_PointFromText', $asStoredPoint);
60
+ $pdo->sqliteCreateFunction('ST_GeomFromText', $asStoredPoint);
61
+ $connection = new SQLiteConnection($pdo);
62
+ $resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
63
+ $resolver->setDefaultConnection('mysql');
64
+ EloquentModel::setConnectionResolver($resolver);
65
+ EloquentModel::setEventDispatcher(new Dispatcher());
66
+ EloquentModel::clearBootedModels();
67
+
68
+ $config = new Repository([
69
+ 'activitylog' => ['enabled' => false, 'default_auth_driver' => null, 'default_log_name' => 'default'],
70
+ 'api' => ['cache' => ['enabled' => false]],
71
+ 'filesystems' => ['default' => 'local'],
72
+ ]);
73
+ app()->instance('config', $config);
74
+ app()->instance(Illuminate\Contracts\Config\Repository::class, $config);
75
+ app()->instance(Spatie\Activitylog\CauserResolver::class, new class extends Spatie\Activitylog\CauserResolver {
76
+ public function __construct()
77
+ {
78
+ }
79
+
80
+ public function resolve(EloquentModel|int|string|null $subject = null): ?EloquentModel
81
+ {
82
+ return null;
83
+ }
84
+ });
85
+ app()->instance('db', new class($connection) {
86
+ public function __construct(public SQLiteConnection $connection)
87
+ {
88
+ }
89
+
90
+ public function connection($name = null): SQLiteConnection
91
+ {
92
+ return $this->connection;
93
+ }
94
+
95
+ public function __call($method, $arguments)
96
+ {
97
+ return $this->connection->{$method}(...$arguments);
98
+ }
99
+ });
100
+ Illuminate\Support\Facades\DB::clearResolvedInstance('db');
101
+ app()->instance('db.schema', $connection->getSchemaBuilder());
102
+ // The link's token and PIN are stored with the `encrypted` cast, which
103
+ // resolves the container's encrypter. A reversible stand-in is enough to
104
+ // show a value goes in encrypted and comes back as it was.
105
+ $encrypter = new class {
106
+ // Eloquent's `encrypted` cast calls encrypt($value, false) and
107
+ // decrypt($value, false); the string variants are here for anything
108
+ // that goes through Crypt::encryptString() instead.
109
+ public function encrypt($value, $serialize = true)
110
+ {
111
+ return 'enc:' . base64_encode($serialize ? serialize($value) : (string) $value);
112
+ }
113
+
114
+ public function decrypt($value, $unserialize = true)
115
+ {
116
+ if (!is_string($value) || !str_starts_with($value, 'enc:')) {
117
+ throw new RuntimeException('Unable to decrypt.');
118
+ }
119
+
120
+ $decoded = base64_decode(substr($value, 4), true);
121
+
122
+ return $unserialize ? unserialize($decoded) : $decoded;
123
+ }
124
+
125
+ public function encryptString($value)
126
+ {
127
+ return $this->encrypt($value, false);
128
+ }
129
+
130
+ public function decryptString($value)
131
+ {
132
+ return $this->decrypt($value, false);
133
+ }
134
+ };
135
+ app()->instance('encrypter', $encrypter);
136
+ Illuminate\Support\Facades\Crypt::clearResolvedInstance('encrypter');
137
+ EloquentModel::encryptUsing($encrypter);
138
+ app()->instance('responsecache', new class {
139
+ public function __call($method, $arguments)
140
+ {
141
+ return null;
142
+ }
143
+ });
144
+ app()->instance('request', Request::create('/'));
145
+
146
+ $schema = $connection->getSchemaBuilder();
147
+ $tables = [
148
+ 'inspection_forms' => ['uuid', 'public_id', '_key', 'company_uuid', 'name', 'description', 'type', 'status', 'subject_type', 'subject_uuid', 'items', 'settings', 'meta', 'published_at', 'created_by_uuid', 'updated_by_uuid'],
149
+ 'inspection_links' => ['uuid', 'public_id', '_key', 'company_uuid', 'inspection_form_uuid', 'driver_uuid', 'vehicle_uuid', 'assignee_uuid', 'created_by_uuid', 'token_hash', 'token', 'pin_hash', 'pin', 'pin_attempts', 'pin_sent_via', 'pin_sent_at', 'status', 'single_use', 'expires_at', 'last_viewed_at', 'used_at', 'used_ip', 'used_user_agent', 'meta'],
150
+ 'inspection_submissions' => ['uuid', 'public_id', '_key', 'company_uuid', 'inspection_form_uuid', 'vehicle_uuid', 'driver_uuid', 'submitted_by_uuid', 'issue_uuid', 'work_order_uuid', 'type', 'status', 'result', 'source', 'odometer', 'engine_hours', 'total_items', 'failed_items', 'started_at', 'submitted_at', 'resolved_at', 'location', 'signature', 'attachments', 'meta', 'created_by_uuid', 'updated_by_uuid'],
151
+ 'inspection_item_results' => ['uuid', '_key', 'company_uuid', 'inspection_submission_uuid', 'issue_uuid', 'work_order_uuid', 'item_key', 'label', 'category', 'status', 'severity', 'passed', 'comments', 'photos', 'meta', 'created_by_uuid', 'updated_by_uuid'],
152
+ 'issues' => ['uuid', 'public_id', '_key', 'company_uuid', 'reported_by_uuid', 'assigned_to_uuid', 'vehicle_uuid', 'driver_uuid', 'order_uuid', 'issue_id', 'location', 'category', 'type', 'report', 'title', 'tags', 'priority', 'meta', 'resolved_at', 'status'],
153
+ 'work_orders' => ['uuid', 'public_id', '_key', 'company_uuid', 'schedule_uuid', 'code', 'subject', 'category', 'status', 'priority', 'target_type', 'target_uuid', 'assignee_type', 'assignee_uuid', 'opened_at', 'due_at', 'closed_at', 'instructions', 'checklist', 'currency', 'estimated_cost', 'approved_budget', 'actual_cost', 'cost_center', 'budget_code', 'meta', 'created_by_uuid', 'updated_by_uuid'],
154
+ 'vehicles' => ['uuid', 'public_id', 'internal_id', '_key', 'company_uuid', 'vendor_uuid', 'photo_uuid', 'name', 'make', 'model', 'year', 'trim', 'plate_number', 'vin', 'status', 'currency', 'slug', 'online', 'location'],
155
+ 'drivers' => ['uuid', 'public_id', 'internal_id', '_key', 'company_uuid', 'user_uuid', 'vehicle_uuid', 'vendor_uuid', 'current_job_uuid', 'photo_uuid', 'status', 'online', 'location', 'slug'],
156
+ 'users' => ['uuid', 'public_id', '_key', 'company_uuid', 'name', 'email', 'phone', 'avatar_uuid', 'type', 'status'],
157
+ 'companies' => ['uuid', 'public_id', '_key', 'name', 'owner_uuid', 'options'],
158
+ 'company_users' => ['uuid', '_key', 'company_uuid', 'user_uuid', 'role_uuid', 'status'],
159
+ 'settings' => ['key', 'value'],
160
+ 'files' => ['uuid', 'public_id', '_key', 'company_uuid', 'uploader_uuid', 'subject_uuid', 'subject_type', 'path', 'disk', 'bucket', 'folder', 'etag', 'meta', 'original_filename', 'type', 'content_type', 'file_size', 'slug', 'caption'],
161
+ 'vendors' => ['uuid', 'public_id', '_key', 'company_uuid', 'name'],
162
+ 'orders' => ['uuid', 'public_id', '_key', 'company_uuid', 'driver_assigned_uuid', 'status'],
163
+ 'positions' => ['uuid', 'public_id', '_key', 'company_uuid', 'subject_uuid', 'subject_type', 'coordinates'],
164
+ 'maintenances' => ['uuid', 'public_id', '_key', 'company_uuid', 'maintainable_type', 'maintainable_uuid', 'status', 'completed_at'],
165
+ 'maintenance_schedules' => ['uuid', 'public_id', '_key', 'company_uuid', 'subject_type', 'subject_uuid', 'status', 'next_due_at'],
166
+ 'custom_field_values' => ['uuid', 'company_uuid', 'custom_field_uuid', 'subject_uuid', 'subject_type', 'value', 'value_type'],
167
+ 'custom_fields' => ['uuid', 'public_id', 'company_uuid', 'category_uuid', 'subject_uuid', 'subject_type', 'name', 'label', 'type', 'for', 'component', 'options', 'required', 'editable', 'default_value', 'validation_rules', 'meta', 'description', 'help_text', 'order'],
168
+ 'categories' => ['uuid', 'public_id', '_key', 'company_uuid', 'owner_uuid', 'owner_type', 'parent_uuid', 'icon_file_uuid', 'internal_id', 'name', 'description', 'translations', 'meta', 'tags', 'icon', 'icon_color', 'slug', 'order', 'for', 'core_category'],
169
+ 'activity_log' => ['uuid', 'company_uuid', 'log_name', 'description', 'subject_type', 'subject_id', 'causer_type', 'causer_id', 'properties', 'event', 'batch_uuid'],
170
+ ];
171
+
172
+ foreach ($tables as $table => $columns) {
173
+ $schema->create($table, function ($blueprint) use ($columns) {
174
+ $blueprint->increments('id');
175
+ foreach ($columns as $column) {
176
+ $blueprint->string($column)->nullable();
177
+ }
178
+ $blueprint->timestamps();
179
+ $blueprint->timestamp('deleted_at')->nullable();
180
+ });
181
+ }
182
+
183
+ $connection->table('companies')->insert(['uuid' => 'company-insp', 'public_id' => 'company_insp', 'name' => 'Inspection Co']);
184
+ $connection->table('companies')->insert(['uuid' => 'company-other', 'public_id' => 'company_other', 'name' => 'Someone Else']);
185
+ $connection->table('users')->insert(['uuid' => 'user-driver', 'public_id' => 'user_driver', 'company_uuid' => 'company-insp', 'name' => 'Dana Driver', 'email' => 'dana@example.com', 'phone' => '+15550001111', 'type' => 'user']);
186
+ $connection->table('users')->insert(['uuid' => 'user-admin', 'public_id' => 'user_admin', 'company_uuid' => 'company-insp', 'name' => 'Avery Admin', 'email' => 'avery@example.com', 'type' => 'user']);
187
+ $connection->table('vehicles')->insert(['uuid' => 'vehicle-1', 'public_id' => 'vehicle_one', 'company_uuid' => 'company-insp', 'name' => 'Truck 7', 'plate_number' => 'TRK-7', 'currency' => 'SGD']);
188
+ $connection->table('vehicles')->insert(['uuid' => 'vehicle-2', 'public_id' => 'vehicle_two', 'company_uuid' => 'company-insp', 'name' => 'Van 2', 'plate_number' => 'VAN-2']);
189
+ $connection->table('vehicles')->insert(['uuid' => 'vehicle-other', 'public_id' => 'vehicle_other', 'company_uuid' => 'company-other', 'name' => 'Not Ours']);
190
+ $connection->table('drivers')->insert(['uuid' => 'driver-1', 'public_id' => 'driver_one', 'company_uuid' => 'company-insp', 'user_uuid' => 'user-driver', 'vehicle_uuid' => 'vehicle-1', 'status' => 'active']);
191
+ $connection->table('drivers')->insert(['uuid' => 'driver-other', 'public_id' => 'driver_other', 'company_uuid' => 'company-other', 'user_uuid' => null, 'status' => 'active']);
192
+
193
+ session(['company' => 'company-insp', 'user' => 'user-admin']);
194
+
195
+ return $connection;
196
+ }
197
+
198
+ function fleetOpsInspectionControllerForm(array $attributes = []): InspectionForm
199
+ {
200
+ return InspectionForm::create(array_merge([
201
+ 'company_uuid' => 'company-insp',
202
+ 'name' => 'Pre-trip DVIR',
203
+ 'type' => 'dvir',
204
+ 'status' => 'published',
205
+ 'published_at' => '2026-09-01 08:00:00',
206
+ 'items' => [
207
+ ['key' => 'brakes', 'label' => 'Brakes', 'category' => 'Safety', 'severity' => 'critical'],
208
+ ['key' => 'lights', 'label' => 'Lights', 'category' => 'Safety', 'severity' => 'medium'],
209
+ ],
210
+ 'settings' => ['create_issue_on_failure' => true, 'create_work_order_on_failure' => true],
211
+ ], $attributes));
212
+ }
213
+
214
+ function fleetOpsInspectionControllerLink(InspectionForm $form, string $token, array $attributes = []): InspectionLink
215
+ {
216
+ return InspectionLink::create(array_merge([
217
+ 'company_uuid' => 'company-insp',
218
+ 'inspection_form_uuid' => $form->uuid,
219
+ 'driver_uuid' => 'driver-1',
220
+ 'vehicle_uuid' => 'vehicle-1',
221
+ 'created_by_uuid' => 'user-admin',
222
+ 'token_hash' => InspectionLink::hashToken($token),
223
+ 'status' => 'active',
224
+ 'single_use' => true,
225
+ ], $attributes));
226
+ }
227
+
228
+ function fleetOpsInspectionControllerBody(array $overrides = []): array
229
+ {
230
+ return array_merge([
231
+ 'odometer' => 120400,
232
+ 'item_results' => [
233
+ ['item_key' => 'brakes', 'label' => 'Brakes', 'passed' => false, 'severity' => 'critical', 'comments' => 'Soft pedal', 'photos' => ['iVBORw0KGgoAAAANSUhEUg==']],
234
+ ['item_key' => 'lights', 'label' => 'Lights', 'passed' => true],
235
+ ],
236
+ ], $overrides);
237
+ }
238
+
239
+ /** Runs a public-link call and hands back the refusal it aborted with, if any. */
240
+ function fleetOpsInspectionControllerRefusal(callable $call): ?JsonResponse
241
+ {
242
+ try {
243
+ $call();
244
+ } catch (HttpResponseException $exception) {
245
+ return $exception->getResponse();
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ /*
252
+ * A stored file's caption is humanized from its name by a core-api macro that
253
+ * is not registered in this harness. The same stand-in the field tests use.
254
+ */
255
+ if (!Illuminate\Support\Str::hasMacro('humanize')) {
256
+ Illuminate\Support\Str::macro('humanize', fn ($value, $uppercase = true) => str_replace('_', ' ', Illuminate\Support\Str::snake((string) $value)));
257
+ }
258
+
259
+ if (!function_exists('event')) {
260
+ /**
261
+ * The platform's event(), fired by core's File observer when a file is
262
+ * recorded. Nothing listens in this harness, so it dispatches nothing.
263
+ */
264
+ function event(...$arguments)
265
+ {
266
+ return null;
267
+ }
268
+ }
269
+
270
+ if (!function_exists('report')) {
271
+ /**
272
+ * The platform's report(), which this package's test bootstrap does not
273
+ * define: hands the exception to whichever handler is bound.
274
+ */
275
+ function report($exception)
276
+ {
277
+ app(Illuminate\Contracts\Debug\ExceptionHandler::class)->report($exception);
278
+ }
279
+ }
280
+
281
+ /** A route that names only its URI, which is all a resource reads to tell internal from public. */
282
+ class FleetOpsInspectionControllerRouteFixture
283
+ {
284
+ public function __construct(private string $uri)
285
+ {
286
+ }
287
+
288
+ public function uri(): string
289
+ {
290
+ return $this->uri;
291
+ }
292
+ }
293
+
294
+ /** Binds an internal request, so resources include what only the console is shown. */
295
+ function fleetOpsInspectionControllerInternalRequest(string $method = 'GET', array $input = []): Request
296
+ {
297
+ $uri = 'int/v1/inspection-forms';
298
+ $request = Request::create('/' . $uri, $method, $input);
299
+ $request->setRouteResolver(fn () => new FleetOpsInspectionControllerRouteFixture($uri));
300
+ app()->instance('request', $request);
301
+
302
+ return $request;
303
+ }
304
+
305
+ /**
306
+ * Swap in a container that answers environment(), which Utils::consoleUrl()
307
+ * asks when a link's address is built. Copied from the driver auth tests.
308
+ */
309
+ function fleetOpsInspectionControllerContainer(): void
310
+ {
311
+ $current = Illuminate\Container\Container::getInstance();
312
+ if (method_exists($current, 'hasDebugModeEnabled')) {
313
+ return;
314
+ }
315
+
316
+ $replacement = new class extends Illuminate\Container\Container {
317
+ public function environment(...$environments)
318
+ {
319
+ if (empty($environments)) {
320
+ return 'testing';
321
+ }
322
+
323
+ $checks = is_array($environments[0]) ? $environments[0] : $environments;
324
+
325
+ return in_array('testing', $checks, true);
326
+ }
327
+
328
+ public function hasDebugModeEnabled()
329
+ {
330
+ return true;
331
+ }
332
+ };
333
+
334
+ foreach (['bindings', 'instances', 'aliases', 'abstractAliases', 'resolved', 'extenders', 'tags', 'contextual', 'scopedInstances', 'reboundCallbacks', 'globalBeforeResolvingCallbacks', 'globalResolvingCallbacks', 'globalAfterResolvingCallbacks', 'beforeResolvingCallbacks', 'resolvingCallbacks', 'afterResolvingCallbacks'] as $property) {
335
+ if (!property_exists(Illuminate\Container\Container::class, $property)) {
336
+ continue;
337
+ }
338
+ $reflection = new ReflectionProperty(Illuminate\Container\Container::class, $property);
339
+ $reflection->setAccessible(true);
340
+ if ($reflection->isInitialized($current)) {
341
+ $reflection->setValue($replacement, $reflection->getValue($current));
342
+ }
343
+ }
344
+
345
+ Illuminate\Container\Container::setInstance($replacement);
346
+ Illuminate\Support\Facades\Facade::setFacadeApplication($replacement);
347
+ }
348
+
349
+ /**
350
+ * Lets a link's PIN actually be sent inside the harness: the console's host, a
351
+ * mailer and an SMS service that record what they are handed, and a handler
352
+ * for report(). Each can be told to fail, through the object returned.
353
+ */
354
+ function fleetOpsInspectionControllerDelivery(): object
355
+ {
356
+ fleetOpsInspectionControllerContainer();
357
+ config(['fleetbase.console.host' => 'console.test', 'fleetbase.console.secure' => true]);
358
+
359
+ $fakes = new class {
360
+ public array $mail = [];
361
+ public array $sms = [];
362
+ public array $reported = [];
363
+ public mixed $to = null;
364
+ public ?Throwable $mailFails = null;
365
+ public ?Throwable $smsFails = null;
366
+ public ?array $smsAnswer = null;
367
+ };
368
+
369
+ Illuminate\Support\Facades\Mail::swap(new class($fakes) {
370
+ public function __construct(private object $fakes)
371
+ {
372
+ }
373
+
374
+ public function to($users)
375
+ {
376
+ $this->fakes->to = $users;
377
+
378
+ return $this;
379
+ }
380
+
381
+ public function send($mailable)
382
+ {
383
+ if ($this->fakes->mailFails) {
384
+ throw $this->fakes->mailFails;
385
+ }
386
+
387
+ $this->fakes->mail[] = $mailable;
388
+
389
+ return null;
390
+ }
391
+
392
+ public function __call($method, $arguments)
393
+ {
394
+ return $this;
395
+ }
396
+ });
397
+
398
+ app()->instance(Fleetbase\Services\SmsService::class, new class($fakes) extends Fleetbase\Services\SmsService {
399
+ public function __construct(private object $fakes)
400
+ {
401
+ }
402
+
403
+ public function send(string $to, string $text, array $options = [], ?string $provider = null): array
404
+ {
405
+ if ($this->fakes->smsFails) {
406
+ throw $this->fakes->smsFails;
407
+ }
408
+
409
+ $this->fakes->sms[] = ['to' => $to, 'text' => $text, 'options' => $options];
410
+
411
+ return $this->fakes->smsAnswer ?? ['success' => true, 'provider' => 'twilio'];
412
+ }
413
+ });
414
+
415
+ app()->instance(Illuminate\Contracts\Debug\ExceptionHandler::class, new class($fakes) {
416
+ public function __construct(private object $fakes)
417
+ {
418
+ }
419
+
420
+ public function report(Throwable $e): void
421
+ {
422
+ $this->fakes->reported[] = $e;
423
+ }
424
+
425
+ public function __call($method, $arguments)
426
+ {
427
+ return null;
428
+ }
429
+ });
430
+
431
+ return $fakes;
432
+ }
433
+
434
+ /** A real disk under a temporary directory, public so a stored file has a URL. */
435
+ function fleetOpsInspectionControllerDisk(): string
436
+ {
437
+ $root = sys_get_temp_dir() . '/fleetops-inspection-uploads-' . bin2hex(random_bytes(4));
438
+ config([
439
+ 'filesystems.default' => 'public',
440
+ 'filesystems.disks.public' => ['driver' => 'local', 'root' => $root, 'url' => 'https://files.test/storage', 'visibility' => 'public'],
441
+ ]);
442
+
443
+ $manager = new Illuminate\Filesystem\FilesystemManager(app());
444
+ app()->instance('filesystem', $manager);
445
+ app()->instance(Illuminate\Contracts\Filesystem\Factory::class, $manager);
446
+ Illuminate\Support\Facades\Storage::clearResolvedInstances();
447
+ Illuminate\Support\Facades\Storage::swap($manager);
448
+
449
+ return $root;
450
+ }
451
+
452
+ /** A one-pixel PNG, under whatever name the device gave it. */
453
+ function fleetOpsInspectionControllerPhoto(string $clientName = 'photo.php'): Illuminate\Http\UploadedFile
454
+ {
455
+ $path = tempnam(sys_get_temp_dir(), 'insp');
456
+ file_put_contents($path, base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='));
457
+
458
+ return new Illuminate\Http\UploadedFile($path, $clientName, 'image/png', null, true);
459
+ }
460
+
461
+ afterEach(function () {
462
+ Carbon::setTestNow();
463
+ });
464
+
465
+ test('public inspection link refuses a missing, unknown, unpublished, invalid or spent token', function () {
466
+ fleetOpsInspectionControllerDatabase();
467
+ $controller = new PublicInspectionController();
468
+ $form = fleetOpsInspectionControllerForm();
469
+ fleetOpsInspectionControllerLink($form, 'good-token');
470
+ fleetOpsInspectionControllerLink($form, 'spent-token', ['used_at' => '2026-09-01 09:00:00']);
471
+
472
+ $refusal = fn (array $query, ?string $id = null) => fleetOpsInspectionControllerRefusal(
473
+ fn () => $controller->show(Request::create('/public/inspections/forms/x', 'GET', $query), $id ?? $form->public_id)
474
+ );
475
+
476
+ expect($refusal([])->getStatusCode())->toBe(403)
477
+ ->and($refusal([])->getData(true))->toBe(['error' => 'Inspection token is required.'])
478
+ ->and($refusal(['token' => 'nope'])->getData(true))->toBe(['error' => 'Inspection link is invalid.'])
479
+ ->and($refusal(['token' => 'spent-token'])->getData(true))->toBe(['error' => 'Inspection link is expired, revoked, or already used.']);
480
+
481
+ $draft = fleetOpsInspectionControllerForm(['status' => 'draft', 'published_at' => null]);
482
+ expect($refusal(['token' => 'good-token'], $draft->public_id)->getData(true))->toBe(['error' => 'This inspection form is not available.']);
483
+
484
+ expect(fn () => $controller->show(Request::create('/public/inspections/forms/x', 'GET', ['token' => 'good-token']), 'inspection_form_missing'))
485
+ ->toThrow(ModelNotFoundException::class);
486
+ });
487
+
488
+ test('public inspection link shows the form with the identity it was minted for', function () {
489
+ fleetOpsInspectionControllerDatabase();
490
+ Carbon::setTestNow('2026-09-09 08:00:00');
491
+ $controller = new PublicInspectionController();
492
+ $form = fleetOpsInspectionControllerForm();
493
+ $link = fleetOpsInspectionControllerLink($form, 'good-token', ['expires_at' => '2026-09-10 08:00:00']);
494
+
495
+ $payload = $controller->show(Request::create('/public/inspections/forms/x', 'GET', ['token' => 'good-token']), $form->uuid)->getData(true);
496
+
497
+ expect($payload['form']['id'])->toBe($form->public_id)
498
+ ->and($payload['form']['items'])->toHaveCount(2)
499
+ ->and($payload['form']['is_published'])->toBeTrue()
500
+ ->and($payload['identity']['driver'])->toBe(['id' => 'driver_one', 'name' => 'Dana Driver'])
501
+ ->and($payload['identity']['vehicle'])->toBe(['id' => 'vehicle_one', 'name' => 'Truck 7', 'plate_number' => 'TRK-7'])
502
+ ->and($payload['identity']['expires_at'])->toStartWith('2026-09-10')
503
+ ->and($link->fresh()->last_viewed_at->toDateTimeString())->toBe('2026-09-09 08:00:00');
504
+
505
+ // A link minted for nobody in particular carries no identity.
506
+ fleetOpsInspectionControllerLink($form, 'anon-token', ['driver_uuid' => null, 'vehicle_uuid' => null]);
507
+ $anonymous = $controller->show(Request::create('/public/inspections/forms/x', 'GET', ['token' => 'anon-token']), $form->public_id)->getData(true);
508
+ expect($anonymous['identity'])->toBe(['assignee' => null, 'driver' => null, 'vehicle' => null, 'expires_at' => null]);
509
+ });
510
+
511
+ test('public inspection link files a submission and spends the link', function () {
512
+ fleetOpsInspectionControllerDatabase();
513
+ Carbon::setTestNow('2026-09-09 08:30:00');
514
+ $controller = new PublicInspectionController();
515
+ $form = fleetOpsInspectionControllerForm();
516
+ InspectionFormSync::convertLegacyItems($form);
517
+ $link = fleetOpsInspectionControllerLink($form, 'good-token');
518
+ $server = ['REMOTE_ADDR' => '203.0.113.9', 'HTTP_USER_AGENT' => 'Safari'];
519
+
520
+ // A link answers the form's fields. The first cut's flat checklist stores
521
+ // photo URLs as given, so it is refused, and refusing it spends nothing.
522
+ $flat = fleetOpsInspectionControllerRefusal(fn () => $controller->submit(
523
+ Request::create('/public/inspections/forms/x/submit', 'POST', fleetOpsInspectionControllerBody(['token' => 'good-token']), [], [], $server),
524
+ $form->public_id
525
+ ));
526
+ expect($flat->getStatusCode())->toBe(422)
527
+ ->and($flat->getData(true)['error'])->toContain('form fields')
528
+ ->and($link->fresh()->used_at)->toBeNull();
529
+
530
+ $request = Request::create('/public/inspections/forms/x/submit', 'POST', [
531
+ 'token' => 'good-token',
532
+ 'odometer' => 120400,
533
+ 'signature' => ['name' => 'Dana Driver'],
534
+ 'custom_field_values' => [
535
+ ['custom_field' => 'brakes', 'value_type' => 'object', 'value' => ['passed' => false, 'severity' => 'critical', 'comments' => 'Soft pedal', 'photos' => []]],
536
+ ['custom_field' => 'lights', 'value_type' => 'object', 'value' => ['passed' => true]],
537
+ ],
538
+ ], [], [], $server);
539
+ $payload = $controller->submit($request, $form->public_id)->getData(true);
540
+
541
+ expect($payload['message'])->toBe('Inspection submitted.')
542
+ ->and($payload['submission']['source'])->toBe('public_link')
543
+ ->and($payload['submission']['status'])->toBe('submitted')
544
+ ->and($payload['submission']['result'])->toBe('failed')
545
+ ->and($payload['submission']['failed_items'])->toBe(1)
546
+ ->and($payload['submission']['meta']['inspection_link_id'])->toBe($link->public_id)
547
+ // Who typed their name, beside the account the link credits, and
548
+ // whether a PIN stood between the link and the form.
549
+ ->and($payload['submission']['meta']['completed_by_name'])->toBe('Dana Driver')
550
+ ->and($payload['submission']['meta']['pin_verified'])->toBeFalse()
551
+ ->and($payload['submission']['item_results'])->toHaveCount(2)
552
+ ->and($payload['submission']['issue']['id'])->toStartWith('issue_')
553
+ ->and($payload['submission']['work_order']['id'])->toStartWith('work_order_')
554
+ ->and($payload['submission']['driver']['id'])->toBe('driver_one')
555
+ ->and($payload['submission']['vehicle']['id'])->toBe('vehicle_one')
556
+ ->and($payload['submission']['form']['id'])->toBe($form->public_id);
557
+
558
+ // Nobody is assigned, so the submission is credited to the driver's account.
559
+ $submission = InspectionSubmission::query()->first();
560
+ expect($submission->submitted_by_uuid)->toBe('user-driver')
561
+ ->and($submission->driver_uuid)->toBe('driver-1')
562
+ ->and($submission->vehicle_uuid)->toBe('vehicle-1');
563
+
564
+ $spent = $link->fresh();
565
+ expect($spent->used_at->toDateTimeString())->toBe('2026-09-09 08:30:00')
566
+ ->and($spent->used_ip)->toBe('203.0.113.9')
567
+ ->and($spent->used_user_agent)->toBe('Safari')
568
+ ->and($spent->isUsable())->toBeFalse();
569
+
570
+ // Spent is spent: the same token cannot file twice.
571
+ expect(fleetOpsInspectionControllerRefusal(fn () => $controller->submit($request, $form->public_id))->getStatusCode())->toBe(403);
572
+ });
573
+
574
+ test('internal inspection form controller publishes archives and mints links', function () {
575
+ fleetOpsInspectionControllerDatabase();
576
+ Carbon::setTestNow('2026-09-09 09:00:00');
577
+ $controller = new InspectionFormController();
578
+ $form = fleetOpsInspectionControllerForm(['status' => 'draft', 'published_at' => null]);
579
+
580
+ // A draft cannot be handed out.
581
+ $refused = $controller->generateLink(Request::create('/', 'POST'), $form->public_id);
582
+ expect($refused->getStatusCode())->toBe(422)
583
+ ->and($refused->getData(true)['error'])->toContain('must be published');
584
+
585
+ $published = $controller->publish($form->public_id)->getData(true);
586
+ expect($published['status'])->toBe('ok')
587
+ ->and($published['data']['status'])->toBe('published')
588
+ ->and($published['data']['is_published'])->toBeTrue();
589
+
590
+ $minted = $controller->generateLink(Request::create('/', 'POST', ['driver' => 'driver_one', 'vehicle' => 'vehicle-1', 'expires_at' => '2026-09-10 09:00:00', 'single_use' => false]), $form->uuid)->getData(true);
591
+ $link = InspectionLink::query()->first();
592
+
593
+ expect($minted['status'])->toBe('ok')
594
+ ->and($minted['link']['id'])->toBe($link->public_id)
595
+ ->and($minted['link']['path'])->toBe('/~/inspection?id=' . urlencode($form->public_id) . '&token=' . urlencode($minted['link']['token']))
596
+ ->and(strlen($minted['link']['token']))->toBe(64)
597
+ ->and($link->token_hash)->toBe(InspectionLink::hashToken($minted['link']['token']))
598
+ ->and($link->driver_uuid)->toBe('driver-1')
599
+ ->and($link->vehicle_uuid)->toBe('vehicle-1')
600
+ ->and($link->created_by_uuid)->toBe('user-admin')
601
+ ->and($link->single_use)->toBeFalse()
602
+ ->and($minted['link']['driver'])->toBe(['id' => 'driver_one', 'name' => 'Dana Driver'])
603
+ ->and($minted['link']['vehicle'])->toBe(['id' => 'vehicle_one', 'name' => 'Truck 7'])
604
+ ->and($minted['link']['expires_at'])->toStartWith('2026-09-10');
605
+
606
+ // Nobody named: a link anyone may use, single use by default.
607
+ $open = $controller->generateLink(Request::create('/', 'POST'), $form->public_id)->getData(true);
608
+ expect($open['link']['driver'])->toBeNull()
609
+ ->and($open['link']['vehicle'])->toBeNull()
610
+ ->and((bool) InspectionLink::query()->where('public_id', $open['link']['id'])->value('single_use'))->toBeTrue();
611
+
612
+ // A driver or vehicle from another company does not exist here.
613
+ expect(fn () => $controller->generateLink(Request::create('/', 'POST', ['driver' => 'driver_other']), $form->public_id))->toThrow(ModelNotFoundException::class)
614
+ ->and(fn () => $controller->generateLink(Request::create('/', 'POST', ['vehicle' => 'vehicle_other']), $form->public_id))->toThrow(ModelNotFoundException::class);
615
+
616
+ $archived = $controller->archive($form->public_id)->getData(true);
617
+ expect($archived['message'])->toBe('Inspection form archived.')
618
+ ->and($archived['data']['status'])->toBe('archived');
619
+
620
+ session(['company' => 'company-other']);
621
+ expect(fn () => $controller->publish($form->public_id))->toThrow(ModelNotFoundException::class);
622
+ });
623
+
624
+ test('internal inspection submission controller syncs item results from the console form', function () {
625
+ fleetOpsInspectionControllerDatabase();
626
+ $controller = new InspectionSubmissionController();
627
+ $form = fleetOpsInspectionControllerForm();
628
+ $submission = InspectionSubmission::create(['company_uuid' => 'company-insp', 'inspection_form_uuid' => $form->uuid, 'vehicle_uuid' => 'vehicle-1', 'driver_uuid' => 'driver-1', 'type' => 'dvir', 'status' => 'draft']);
629
+
630
+ // Nothing sent: nothing touched.
631
+ $controller->onAfterCreate(Request::create('/', 'POST'), $submission, []);
632
+ expect(InspectionItemResult::query()->count())->toBe(0);
633
+
634
+ // The console nests the payload under the resource name.
635
+ $controller->onAfterCreate(Request::create('/', 'POST', ['inspection_submission' => ['item_results' => [
636
+ ['item_key' => 'brakes', 'label' => 'Brakes', 'passed' => false, 'severity' => 'critical'],
637
+ ['title' => 'Untitled item', 'status' => 'failed'],
638
+ ['label' => 'Horn'],
639
+ ]]]), $submission, []);
640
+
641
+ $results = $submission->itemResults()->orderBy('id')->get();
642
+ expect($results)->toHaveCount(3)
643
+ ->and($results[0]->status)->toBe('failed')
644
+ ->and($results[1]->label)->toBe('Untitled item')
645
+ ->and($results[1]->passed)->toBeFalse()
646
+ // No status and no flag: passed, as an unchecked box is.
647
+ ->and($results[2]->status)->toBe('passed')
648
+ ->and($results[2]->passed)->toBeTrue()
649
+ ->and($submission->fresh()->failed_items)->toBe(2)
650
+ ->and($submission->relationLoaded('itemResults'))->toBeTrue();
651
+
652
+ // An update matches by uuid, then item key, then label — and drops what
653
+ // the console no longer lists.
654
+ $controller->onAfterUpdate(Request::create('/', 'PUT', ['item_results' => [
655
+ ['uuid' => $results[0]->uuid, 'item_key' => 'brakes', 'label' => 'Brakes', 'passed' => true],
656
+ ['label' => 'Horn', 'passed' => false, 'status' => 'failed', 'meta' => ['note' => 'weak']],
657
+ ]]), $submission->fresh(), []);
658
+
659
+ $synced = $submission->fresh();
660
+ expect($synced->itemResults()->count())->toBe(2)
661
+ ->and((bool) $synced->itemResults()->where('uuid', $results[0]->uuid)->value('passed'))->toBeTrue()
662
+ ->and($synced->itemResults()->where('label', 'Untitled item')->exists())->toBeFalse()
663
+ ->and($synced->itemResults()->where('label', 'Horn')->first()->meta['note'])->toBe('weak')
664
+ ->and($synced->failed_items)->toBe(1);
665
+
666
+ $builder = InspectionSubmission::query();
667
+ $controller->onFindRecord($builder, Request::create('/'));
668
+ expect(array_keys($builder->getEagerLoads()))->toContain('form', 'vehicle', 'driver', 'submittedBy', 'issue', 'workOrder', 'itemResults');
669
+ });
670
+
671
+ test('internal inspection submission controller submits, raises follow-up and resolves', function () {
672
+ fleetOpsInspectionControllerDatabase();
673
+ Carbon::setTestNow('2026-09-09 10:00:00');
674
+ $controller = new InspectionSubmissionController();
675
+ $form = fleetOpsInspectionControllerForm();
676
+
677
+ $clean = InspectionSubmission::create(['company_uuid' => 'company-insp', 'inspection_form_uuid' => $form->uuid, 'vehicle_uuid' => 'vehicle-1', 'driver_uuid' => 'driver-1', 'type' => 'dvir', 'status' => 'draft']);
678
+ InspectionItemResult::create(['company_uuid' => 'company-insp', 'inspection_submission_uuid' => $clean->uuid, 'label' => 'Brakes', 'passed' => true]);
679
+
680
+ $submitted = $controller->submit($clean->public_id)->getData(true);
681
+ expect($submitted['message'])->toBe('Inspection submitted.')
682
+ ->and($submitted['data']['status'])->toBe('submitted')
683
+ ->and($submitted['data']['result'])->toBe('passed');
684
+
685
+ // Nothing failed, nothing to raise.
686
+ $noIssue = $controller->createIssue($clean->uuid)->getData(true);
687
+ expect($noIssue['message'])->toBe('No failed inspection items found.')
688
+ ->and($noIssue['issue'])->toBeNull();
689
+ $noOrder = $controller->createWorkOrder($clean->uuid)->getData(true);
690
+ expect($noOrder['message'])->toBe('No failed inspection items found.')
691
+ ->and($noOrder['work_order'])->toBeNull();
692
+
693
+ $failed = InspectionSubmission::create(['company_uuid' => 'company-insp', 'inspection_form_uuid' => $form->uuid, 'vehicle_uuid' => 'vehicle-1', 'driver_uuid' => 'driver-1', 'submitted_by_uuid' => 'user-driver', 'type' => 'dvir', 'status' => 'draft']);
694
+ InspectionItemResult::create(['company_uuid' => 'company-insp', 'inspection_submission_uuid' => $failed->uuid, 'label' => 'Brakes', 'passed' => false, 'severity' => 'high']);
695
+
696
+ $issued = $controller->createIssue($failed->public_id)->getData(true);
697
+ expect($issued['message'])->toBe('Issue created from failed inspection items.')
698
+ ->and($issued['issue']['title'])->toBe('Failed inspection: Truck 7')
699
+ ->and($issued['data']['issue']['uuid'])->toBe($issued['issue']['uuid']);
700
+
701
+ $ordered = $controller->createWorkOrder($failed->public_id)->getData(true);
702
+ expect($ordered['message'])->toBe('Work order created from failed inspection items.')
703
+ ->and($ordered['work_order']['subject'])->toBe('Inspection repair: Truck 7')
704
+ ->and($ordered['work_order']['meta']['issue_uuid'])->toBe($issued['issue']['uuid'])
705
+ ->and(Issue::query()->count())->toBe(1)
706
+ ->and(WorkOrder::query()->count())->toBe(1);
707
+
708
+ $resolved = $controller->resolve($failed->public_id)->getData(true);
709
+ expect($resolved['message'])->toBe('Inspection resolved.')
710
+ ->and($resolved['data']['status'])->toBe('resolved')
711
+ ->and($failed->fresh()->resolved_at->toDateTimeString())->toBe('2026-09-09 10:00:00');
712
+
713
+ expect(fn () => $controller->submit('inspection_submission_missing'))->toThrow(ModelNotFoundException::class);
714
+ });
715
+
716
+ test('driver inspection api lists only the published forms a vehicle can be inspected against', function () {
717
+ fleetOpsInspectionControllerDatabase();
718
+ $controller = new InspectionController();
719
+
720
+ $wide = fleetOpsInspectionControllerForm(['name' => 'Fleet-wide pre-trip', 'type' => 'pre_trip', 'published_at' => '2026-09-01 08:00:00']);
721
+ $truck = fleetOpsInspectionControllerForm(['name' => 'Truck 7 lift gate', 'subject_type' => Vehicle::class, 'subject_uuid' => 'vehicle-1', 'published_at' => '2026-09-03 08:00:00']);
722
+ fleetOpsInspectionControllerForm(['name' => 'Van 2 only', 'subject_type' => Vehicle::class, 'subject_uuid' => 'vehicle-2', 'published_at' => '2026-09-02 08:00:00']);
723
+ fleetOpsInspectionControllerForm(['name' => 'Still a draft', 'status' => 'draft', 'published_at' => null]);
724
+ fleetOpsInspectionControllerForm(['name' => 'Says published, never was', 'status' => 'published', 'published_at' => null]);
725
+ fleetOpsInspectionControllerForm(['name' => 'Retired', 'status' => 'archived']);
726
+ fleetOpsInspectionControllerForm(['name' => 'Another company', 'company_uuid' => 'company-other']);
727
+
728
+ $all = $controller->queryForms(Request::create('/v1/inspection-forms', 'GET'))->resolve();
729
+ expect(array_column($all, 'name'))->toBe(['Truck 7 lift gate', 'Van 2 only', 'Fleet-wide pre-trip'])
730
+ ->and($all[0]['id'])->toBe($truck->public_id)
731
+ ->and($all[0])->not->toHaveKey('uuid');
732
+
733
+ $forTruck = $controller->queryForms(Request::create('/v1/inspection-forms', 'GET', ['vehicle' => 'vehicle_one']))->resolve();
734
+ expect(array_column($forTruck, 'name'))->toBe(['Truck 7 lift gate', 'Fleet-wide pre-trip']);
735
+
736
+ $preTrip = $controller->queryForms(Request::create('/v1/inspection-forms', 'GET', ['type' => 'pre_trip,post_trip', 'limit' => 1]))->resolve();
737
+ expect(array_column($preTrip, 'id'))->toBe([$wide->public_id]);
738
+
739
+ $capped = $controller->queryForms(Request::create('/v1/inspection-forms', 'GET', ['limit' => 2]))->resolve();
740
+ expect($capped)->toHaveCount(2);
741
+
742
+ expect($controller->queryForms(Request::create('/v1/inspection-forms', 'GET', ['vehicle' => 'vehicle_other']))->getStatusCode())->toBe(404);
743
+ });
744
+
745
+ test('driver inspection api shows a published form with its items and settings', function () {
746
+ fleetOpsInspectionControllerDatabase();
747
+ $controller = new InspectionController();
748
+
749
+ $form = fleetOpsInspectionControllerForm(['subject_type' => Vehicle::class, 'subject_uuid' => 'vehicle-1', 'meta' => ['sla' => 'am']]);
750
+ $draft = fleetOpsInspectionControllerForm(['status' => 'draft', 'published_at' => null]);
751
+ $other = fleetOpsInspectionControllerForm(['company_uuid' => 'company-other']);
752
+
753
+ $shown = $controller->findForm($form->public_id)->resolve();
754
+ expect($shown['id'])->toBe($form->public_id)
755
+ ->and($shown['name'])->toBe('Pre-trip DVIR')
756
+ ->and($shown['type'])->toBe('dvir')
757
+ ->and($shown['items'])->toHaveCount(2)
758
+ ->and($shown['item_count'])->toBe(2)
759
+ ->and($shown['settings'])->toBe(['create_issue_on_failure' => true, 'create_work_order_on_failure' => true])
760
+ ->and($shown['meta'])->toBe(['sla' => 'am'])
761
+ ->and($shown['is_published'])->toBeTrue()
762
+ ->and($shown['subject_name'])->toBe('Truck 7')
763
+ ->and($shown['subject']['id'])->toBe('vehicle_one')
764
+ ->and($shown['subject']['type'])->toBe('maintenance-subject-vehicle')
765
+ ->and($shown['subject']['subject_type'])->toBe('maintenance-subject-vehicle');
766
+
767
+ // By uuid as well as public id.
768
+ expect($controller->findForm($form->uuid)->resolve()['id'])->toBe($form->public_id);
769
+
770
+ // A form bound to nothing has no subject to describe.
771
+ $wide = fleetOpsInspectionControllerForm();
772
+ expect($controller->findForm($wide->public_id)->resolve()['subject'])->toBeNull();
773
+
774
+ // Draft, other company, or nothing at all: all the same 404.
775
+ expect($controller->findForm($draft->public_id)->getStatusCode())->toBe(404)
776
+ ->and($controller->findForm($other->public_id)->getStatusCode())->toBe(404)
777
+ ->and($controller->findForm('inspection_form_missing')->getStatusCode())->toBe(404)
778
+ ->and($controller->findForm('inspection_form_missing')->getData(true))->toBe(['error' => 'Inspection form resource not found.']);
779
+ });
780
+
781
+ test('driver inspection api files a submission and answers with its follow-up', function () {
782
+ fleetOpsInspectionControllerDatabase();
783
+ Carbon::setTestNow('2026-09-09 07:30:00');
784
+ $controller = new InspectionController();
785
+ $form = fleetOpsInspectionControllerForm();
786
+
787
+ $body = fleetOpsInspectionControllerBody([
788
+ 'inspection_form' => $form->public_id,
789
+ 'driver' => 'driver_one',
790
+ 'vehicle' => 'vehicle_two',
791
+ 'started_at' => '2026-09-09T07:10:00Z',
792
+ 'location' => ['latitude' => 1.35, 'longitude' => 103.82],
793
+ ]);
794
+
795
+ $filed = $controller->submit(Request::create('/v1/inspections', 'POST', $body))->resolve();
796
+
797
+ expect($filed['id'])->toStartWith('inspection_submission_')
798
+ ->and($filed)->not->toHaveKey('uuid')
799
+ ->and($filed['source'])->toBe('navigator')
800
+ ->and($filed['status'])->toBe('submitted')
801
+ ->and($filed['result'])->toBe('failed')
802
+ ->and($filed['type'])->toBe('dvir')
803
+ ->and($filed['odometer'])->toBe(120400)
804
+ ->and($filed['total_items'])->toBe(2)
805
+ ->and($filed['failed_items'])->toBe(1)
806
+ ->and($filed['has_failures'])->toBeTrue()
807
+ ->and($filed['location'])->toBe(['latitude' => 1.35, 'longitude' => 103.82])
808
+ ->and($filed['started_at']->toDateTimeString())->toBe('2026-09-09 07:10:00')
809
+ ->and($filed['submitted_at']->toDateTimeString())->toBe('2026-09-09 07:30:00')
810
+ ->and($filed['form']->resolve()['id'])->toBe($form->public_id)
811
+ ->and($filed['vehicle']->resolve()['id'])->toBe('vehicle_two')
812
+ ->and($filed['driver']->resolve()['id'])->toBe('driver_one')
813
+ ->and($filed['item_results']->resolve())->toHaveCount(2)
814
+ ->and($filed['item_results']->resolve()[0])->toMatchArray(['item_key' => 'brakes', 'passed' => false, 'status' => 'failed', 'comments' => 'Soft pedal', 'photos' => ['iVBORw0KGgoAAAANSUhEUg==']])
815
+ ->and($filed['item_results']->resolve()[0]['submission_id'])->toBe($filed['id'])
816
+ ->and($filed['issue']->resolve()['id'])->toStartWith('issue_')
817
+ ->and($filed['work_order']->resolve()['id'])->toStartWith('work_order_');
818
+
819
+ $submission = InspectionSubmission::query()->first();
820
+ expect($submission->submitted_by_uuid)->toBe('user-driver')
821
+ ->and($submission->driver_uuid)->toBe('driver-1')
822
+ ->and($submission->vehicle_uuid)->toBe('vehicle-2')
823
+ ->and($submission->meta)->toBe([]);
824
+
825
+ // No vehicle named: the driver's own truck, and no start time: now.
826
+ $assigned = $controller->submit(Request::create('/v1/inspections', 'POST', fleetOpsInspectionControllerBody([
827
+ 'inspection_form' => $form->uuid,
828
+ 'driver' => 'driver-1',
829
+ 'item_results' => [['label' => 'Horn', 'passed' => true]],
830
+ ])))->resolve();
831
+ expect($assigned['vehicle']->resolve()['id'])->toBe('vehicle_one')
832
+ ->and($assigned['result'])->toBe('passed')
833
+ ->and($assigned['started_at']->toDateTimeString())->toBe('2026-09-09 07:30:00')
834
+ ->and($assigned['issue'])->toBeNull()
835
+ ->and($assigned['work_order'])->toBeNull();
836
+ });
837
+
838
+ test('driver inspection api answers a replayed submit with the submission it already filed', function () {
839
+ fleetOpsInspectionControllerDatabase();
840
+ $controller = new InspectionController();
841
+ $form = fleetOpsInspectionControllerForm();
842
+ $body = fleetOpsInspectionControllerBody(['inspection_form' => $form->public_id, 'driver' => 'driver_one']);
843
+ $replay = fn (string $key) => $controller->submit(Request::create('/v1/inspections', 'POST', $body, [], [], ['HTTP_IDEMPOTENCY_KEY' => $key]))->resolve();
844
+
845
+ $first = $replay('queued-7f3a');
846
+ expect(InspectionSubmission::query()->count())->toBe(1)
847
+ ->and(InspectionSubmission::query()->first()->meta)->toBe(['idempotency_key' => 'queued-7f3a']);
848
+
849
+ // The app lost the response and sent the same queued submit again.
850
+ $second = $replay('queued-7f3a');
851
+ expect($second['id'])->toBe($first['id'])
852
+ ->and($second['item_results']->resolve())->toHaveCount(2)
853
+ ->and(InspectionSubmission::query()->count())->toBe(1)
854
+ ->and(Issue::query()->count())->toBe(1);
855
+
856
+ // A different key is a different inspection, and the blank header is no key at all.
857
+ $third = $replay('queued-9c11');
858
+ expect($third['id'])->not->toBe($first['id'])
859
+ ->and(InspectionSubmission::query()->count())->toBe(2);
860
+ $replay(' ');
861
+ expect(InspectionSubmission::query()->count())->toBe(3);
862
+ });
863
+
864
+ test('driver inspection api refuses a submit for a driver, form or vehicle it cannot find', function () {
865
+ fleetOpsInspectionControllerDatabase();
866
+ $controller = new InspectionController();
867
+ $form = fleetOpsInspectionControllerForm();
868
+ $draft = fleetOpsInspectionControllerForm(['status' => 'draft', 'published_at' => null]);
869
+ $submit = fn (array $overrides) => $controller->submit(Request::create('/v1/inspections', 'POST', fleetOpsInspectionControllerBody(array_merge(['inspection_form' => $form->public_id, 'driver' => 'driver_one'], $overrides))));
870
+
871
+ expect($submit(['driver' => 'driver_other'])->getData(true))->toBe(['error' => 'Driver resource not found.'])
872
+ ->and($submit(['driver' => 'driver_other'])->getStatusCode())->toBe(404)
873
+ ->and($submit(['inspection_form' => $draft->public_id])->getData(true))->toBe(['error' => 'Inspection form resource not found.'])
874
+ ->and($submit(['vehicle' => 'vehicle_other'])->getData(true))->toBe(['error' => 'Vehicle resource not found.'])
875
+ ->and(InspectionSubmission::query()->count())->toBe(0);
876
+ });
877
+
878
+ test('driver inspection api lists and shows submissions with their filters', function () {
879
+ fleetOpsInspectionControllerDatabase();
880
+ $controller = new InspectionController();
881
+ $form = fleetOpsInspectionControllerForm(['settings' => []]);
882
+ $postTrip = fleetOpsInspectionControllerForm(['type' => 'post_trip', 'settings' => []]);
883
+
884
+ $file = function (InspectionForm $form, string $vehicle, bool $passed, string $at) use ($controller) {
885
+ Carbon::setTestNow($at);
886
+
887
+ return $controller->submit(Request::create('/v1/inspections', 'POST', [
888
+ 'inspection_form' => $form->public_id,
889
+ 'driver' => 'driver_one',
890
+ 'vehicle' => $vehicle,
891
+ 'item_results' => [['label' => 'Brakes', 'passed' => $passed]],
892
+ ]))->resolve()['id'];
893
+ };
894
+
895
+ $oldest = $file($form, 'vehicle_one', true, '2026-09-07 08:00:00');
896
+ $middle = $file($postTrip, 'vehicle_two', false, '2026-09-08 08:00:00');
897
+ $newest = $file($form, 'vehicle_one', false, '2026-09-09 08:00:00');
898
+
899
+ // Someone else's inspection never shows, whatever the filter.
900
+ InspectionSubmission::create(['company_uuid' => 'company-other', 'inspection_form_uuid' => $form->uuid, 'driver_uuid' => 'driver-other', 'type' => 'dvir', 'status' => 'submitted', 'submitted_at' => '2026-09-10 08:00:00']);
901
+
902
+ $ids = fn (Request $request) => array_column($controller->query($request)->resolve(), 'id');
903
+
904
+ expect($ids(Request::create('/v1/inspections', 'GET')))->toBe([$newest, $middle, $oldest])
905
+ ->and($ids(Request::create('/v1/inspections', 'GET', ['driver' => 'driver_one', 'limit' => 2])))->toBe([$newest, $middle])
906
+ ->and($ids(Request::create('/v1/inspections', 'GET', ['vehicle' => 'vehicle_two'])))->toBe([$middle])
907
+ ->and($ids(Request::create('/v1/inspections', 'GET', ['type' => 'post_trip'])))->toBe([$middle])
908
+ ->and($ids(Request::create('/v1/inspections', 'GET', ['result' => 'passed'])))->toBe([$oldest])
909
+ ->and($ids(Request::create('/v1/inspections', 'GET', ['status' => 'resolved,needs_review'])))->toBe([])
910
+ ->and($ids(Request::create('/v1/inspections', 'GET', ['limit' => 0])))->toBe([$newest, $middle, $oldest]);
911
+
912
+ expect($controller->query(Request::create('/v1/inspections', 'GET', ['driver' => 'driver_other']))->getStatusCode())->toBe(404)
913
+ ->and($controller->query(Request::create('/v1/inspections', 'GET', ['vehicle' => 'vehicle_other']))->getStatusCode())->toBe(404);
914
+
915
+ $shown = $controller->find($newest)->resolve();
916
+ expect($shown['id'])->toBe($newest)
917
+ ->and($shown['item_results']->resolve())->toHaveCount(1)
918
+ ->and($shown['vehicle']->resolve()['id'])->toBe('vehicle_one')
919
+ ->and($shown['driver']->resolve()['id'])->toBe('driver_one')
920
+ ->and($shown['form']->resolve()['id'])->toBe($form->public_id)
921
+ ->and($shown['issue'])->toBeNull()
922
+ ->and($shown['work_order'])->toBeNull();
923
+
924
+ $byUuid = $controller->find(InspectionSubmission::query()->where('public_id', $oldest)->value('uuid'))->resolve();
925
+ expect($byUuid['id'])->toBe($oldest);
926
+
927
+ $foreign = InspectionSubmission::query()->where('company_uuid', 'company-other')->first();
928
+ expect($controller->find($foreign->public_id)->getStatusCode())->toBe(404)
929
+ ->and($controller->find('inspection_submission_missing')->getData(true))->toBe(['error' => 'Inspection resource not found.']);
930
+
931
+ $history = fn (string $vehicle, array $query = []) => array_column($controller->forVehicle(Request::create('/v1/vehicles/x/inspections', 'GET', $query), $vehicle)->resolve(), 'id');
932
+ expect($history('vehicle_one'))->toBe([$newest, $oldest])
933
+ ->and($history('vehicle-1', ['result' => 'failed']))->toBe([$newest])
934
+ ->and($history('vehicle_one', ['limit' => 1]))->toBe([$newest])
935
+ ->and($history('vehicle_two', ['type' => 'dvir']))->toBe([])
936
+ ->and($controller->forVehicle(Request::create('/v1/vehicles/x/inspections', 'GET'), 'vehicle_other')->getStatusCode())->toBe(404);
937
+ });
938
+
939
+ test('inspection form resource keeps internal identifiers off the driver api', function () {
940
+ fleetOpsInspectionControllerDatabase();
941
+ $form = fleetOpsInspectionControllerForm(['subject_type' => Vehicle::class, 'subject_uuid' => 'vehicle-1']);
942
+
943
+ $resolved = (new InspectionFormResource($form->fresh()))->resolve();
944
+
945
+ expect($resolved['id'])->toBe($form->public_id)
946
+ ->and(array_keys($resolved))->not->toContain('uuid', 'public_id', 'company_uuid', 'subject_uuid', 'subject_type', 'created_by_uuid', 'updated_by_uuid')
947
+ ->and($resolved['subject']['type'])->toBe('maintenance-subject-vehicle');
948
+
949
+ // A form bound to nothing, with the relation resolved as such: no subject
950
+ // to transform, and nothing to stamp a type onto.
951
+ $wide = fleetOpsInspectionControllerForm();
952
+ $wide->setRelation('subject', null);
953
+ expect((new InspectionFormResource($wide))->resolve()['subject'])->toBeNull();
954
+
955
+ // `whenLoaded` answers a loaded-null relation itself, so the two helpers
956
+ // never see a missing subject through the resource. Reached directly, as
957
+ // the other morph resources are, so their guards are exercised.
958
+ $resource = new InspectionFormResource($wide);
959
+ $transform = new ReflectionMethod(InspectionFormResource::class, 'transformMorphResource');
960
+ $stamp = new ReflectionMethod(InspectionFormResource::class, 'setSubjectType');
961
+ $transform->setAccessible(true);
962
+ $stamp->setAccessible(true);
963
+
964
+ expect($transform->invoke($resource, null))->toBeNull()
965
+ ->and($stamp->invoke($resource, null))->toBeNull()
966
+ ->and($stamp->invoke($resource, []))->toBe([]);
967
+ });
968
+
969
+ test('inspection link pin names its recipient, says why it cannot send, and builds its message', function () {
970
+ fleetOpsInspectionControllerDatabase();
971
+ fleetOpsInspectionControllerDelivery();
972
+ $form = fleetOpsInspectionControllerForm();
973
+
974
+ $forDriver = fleetOpsInspectionControllerLink($form, 'driver-token');
975
+ $forAdmin = fleetOpsInspectionControllerLink($form, 'admin-token', ['assignee_uuid' => 'user-admin', 'driver_uuid' => null]);
976
+ $forNobody = fleetOpsInspectionControllerLink($form, 'open-token', ['driver_uuid' => null]);
977
+
978
+ // The assignee first, else the driver's own account, else nobody.
979
+ $admin = Fleetbase\FleetOps\Support\InspectionLinkPin::recipientFor($forAdmin->fresh());
980
+ $driver = Fleetbase\FleetOps\Support\InspectionLinkPin::recipientFor($forDriver->fresh());
981
+ expect($admin->uuid)->toBe('user-admin')
982
+ ->and($driver->uuid)->toBe('user-driver')
983
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::recipientFor($forNobody->fresh()))->toBeNull();
984
+
985
+ $nameless = (new Fleetbase\Models\User())->forceFill(['name' => null, 'email' => null, 'phone' => null]);
986
+ expect(Fleetbase\FleetOps\Support\InspectionLinkPin::unavailableReason(null, 'email'))->toContain('Assign the link to someone')
987
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::unavailableReason($admin, 'sms'))->toBe('Avery Admin has no phone number to text the PIN to.')
988
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::unavailableReason($nameless, 'email'))->toBe('This person has no email address to send the PIN to.')
989
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::unavailableReason($driver, 'sms'))->toBeNull()
990
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::unavailableReason($driver, 'email'))->toBeNull();
991
+
992
+ // Enough of where it went to recognise, not enough to copy.
993
+ expect(Fleetbase\FleetOps\Support\InspectionLinkPin::maskEmail('dana@example.com'))->toBe('d•••@example.com')
994
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::maskPhone('+1 (555) 000-1111'))->toBe('•••1111');
995
+
996
+ // The text names the organisation, and carries the link when there is one.
997
+ expect(Fleetbase\FleetOps\Support\InspectionLinkPin::smsText($forDriver, '123456', 'https://console.test/x'))
998
+ ->toBe('Inspection Co: complete the Pre-trip DVIR inspection at https://console.test/x using PIN 123456. Do not share this PIN.')
999
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::smsText($forDriver, '123456'))
1000
+ ->toBe('Inspection Co: your PIN for the Pre-trip DVIR inspection is 123456. Do not share this PIN.');
1001
+
1002
+ // A link's address is on the console's host; one minted before tokens were kept has none.
1003
+ $kept = fleetOpsInspectionControllerLink($form, 'kept-token', ['token' => 'kept-token']);
1004
+ expect(Fleetbase\FleetOps\Support\InspectionLinkPin::urlFor($kept->fresh()))->toBe('https://console.test/~/inspection?id=' . urlencode($form->public_id) . '&token=kept-token')
1005
+ ->and(Fleetbase\FleetOps\Support\InspectionLinkPin::urlFor($forDriver->fresh()))->toBeNull();
1006
+ });
1007
+
1008
+ test('inspection link pin is emailed or texted, and says why when it is not', function () {
1009
+ $connection = fleetOpsInspectionControllerDatabase();
1010
+ Carbon::setTestNow('2026-09-09 09:00:00');
1011
+ $fakes = fleetOpsInspectionControllerDelivery();
1012
+ $form = fleetOpsInspectionControllerForm();
1013
+ $link = fleetOpsInspectionControllerLink($form, 'good-token', ['token' => 'good-token']);
1014
+ $send = fn (string $via) => Fleetbase\FleetOps\Support\InspectionLinkPin::send($link->fresh(), $via);
1015
+
1016
+ // Nothing to send before the link has a PIN.
1017
+ expect($send('email'))->toBe(['sent' => false, 'via' => 'email', 'to' => null, 'error' => 'This link has no PIN to send.']);
1018
+
1019
+ $link->setPin('246810');
1020
+ $link->save();
1021
+ $url = 'https://console.test/~/inspection?id=' . urlencode($form->public_id) . '&token=good-token';
1022
+
1023
+ // By email: the link and the PIN, to the driver's account.
1024
+ expect($send('email'))->toBe(['sent' => true, 'via' => 'email', 'to' => 'd•••@example.com', 'error' => null])
1025
+ ->and($fakes->mail)->toHaveCount(1)
1026
+ ->and($fakes->to->uuid)->toBe('user-driver')
1027
+ ->and($link->fresh()->pin_sent_via)->toBe('email')
1028
+ ->and($link->fresh()->pin_sent_at->toDateTimeString())->toBe('2026-09-09 09:00:00');
1029
+
1030
+ $mail = $fakes->mail[0];
1031
+ expect($mail)->toBeInstanceOf(Fleetbase\FleetOps\Mail\InspectionLinkPinMail::class)
1032
+ ->and($mail->envelope()->subject)->toBe('Complete the Pre-trip DVIR inspection');
1033
+ $content = $mail->content();
1034
+ expect($content->markdown)->toBe('fleetops::mail.inspection-link-pin')
1035
+ ->and($content->with['pin'])->toBe('246810')
1036
+ ->and($content->with['url'])->toBe($url)
1037
+ ->and($content->with['recipient']->uuid)->toBe('user-driver')
1038
+ ->and($content->with['form']->uuid)->toBe($form->uuid)
1039
+ ->and($content->with['vehicle']->uuid)->toBe('vehicle-1')
1040
+ ->and($content->with['sender']->uuid)->toBe('user-admin')
1041
+ ->and($content->with['maxAttempts'])->toBe(InspectionLink::MAX_PIN_ATTEMPTS);
1042
+
1043
+ // By SMS, from the organisation's alphanumeric sender when it has one.
1044
+ $connection->table('companies')->where('uuid', 'company-insp')->update(['options' => json_encode(['alpha_numeric_sender_id_enabled' => true, 'alpha_numeric_sender_id' => 'InspCo'])]);
1045
+ expect($send('sms'))->toBe(['sent' => true, 'via' => 'sms', 'to' => '•••1111', 'error' => null])
1046
+ ->and($fakes->sms[0]['to'])->toBe('+15550001111')
1047
+ ->and($fakes->sms[0]['text'])->toBe('Inspection Co: complete the Pre-trip DVIR inspection at ' . $url . ' using PIN 246810. Do not share this PIN.')
1048
+ ->and($fakes->sms[0]['options'])->toBe(['twilioParams' => ['from' => 'InspCo']])
1049
+ ->and($link->fresh()->pin_sent_via)->toBe('sms');
1050
+
1051
+ // Without the sender switched on, the provider's own number is used.
1052
+ $connection->table('companies')->where('uuid', 'company-insp')->update(['options' => json_encode(['alpha_numeric_sender_id' => 'InspCo'])]);
1053
+ $send('sms');
1054
+ expect($fakes->sms[1]['options'])->toBe([]);
1055
+
1056
+ // A provider that refuses, and a delivery that fails in transit, are
1057
+ // reported back rather than thrown.
1058
+ $fakes->smsAnswer = ['success' => false, 'error' => 'Number is blocked'];
1059
+ expect($send('sms'))->toBe(['sent' => false, 'via' => 'sms', 'to' => null, 'error' => 'The PIN could not be texted: Number is blocked']);
1060
+
1061
+ $fakes->smsAnswer = null;
1062
+ $fakes->smsFails = new RuntimeException('Twilio is down');
1063
+ expect($send('sms'))->toBe(['sent' => false, 'via' => 'sms', 'to' => null, 'error' => 'The PIN could not be sent: Twilio is down'])
1064
+ ->and($fakes->reported)->toHaveCount(1);
1065
+
1066
+ $fakes->mailFails = new RuntimeException('SMTP refused');
1067
+ expect($send('email')['error'])->toBe('The PIN could not be sent: SMTP refused');
1068
+
1069
+ // Someone with no phone cannot be texted.
1070
+ $forAdmin = fleetOpsInspectionControllerLink($form, 'admin-token', ['assignee_uuid' => 'user-admin']);
1071
+ $forAdmin->setPin('112233');
1072
+ $forAdmin->save();
1073
+ expect(Fleetbase\FleetOps\Support\InspectionLinkPin::send($forAdmin->fresh(), 'sms')['error'])->toBe('Avery Admin has no phone number to text the PIN to.');
1074
+
1075
+ // A link whose organisation is gone still sends, with no sender option.
1076
+ $orphan = fleetOpsInspectionControllerLink($form, 'orphan-token', ['company_uuid' => 'company-gone']);
1077
+ $orphan->setPin('998877');
1078
+ $orphan->save();
1079
+ $fakes->smsFails = null;
1080
+ expect(Fleetbase\FleetOps\Support\InspectionLinkPin::send($orphan->fresh(), 'sms')['sent'])->toBeTrue()
1081
+ ->and(end($fakes->sms)['options'])->toBe([]);
1082
+ });
1083
+
1084
+ test('internal inspection form controller assigns links and sends their pin', function () {
1085
+ $connection = fleetOpsInspectionControllerDatabase();
1086
+ Carbon::setTestNow('2026-09-09 09:00:00');
1087
+ $fakes = fleetOpsInspectionControllerDelivery();
1088
+
1089
+ foreach (['user-admin', 'user-driver'] as $user) {
1090
+ $connection->table('company_users')->insert(['uuid' => 'cu-' . $user, 'company_uuid' => 'company-insp', 'user_uuid' => $user, 'status' => 'active']);
1091
+ }
1092
+ $connection->table('users')->insert(['uuid' => 'user-outsider', 'public_id' => 'user_outsider', 'company_uuid' => 'company-other', 'name' => 'Olly Outsider', 'email' => 'olly@example.com', 'type' => 'user']);
1093
+ $connection->table('company_users')->insert(['uuid' => 'cu-outsider', 'company_uuid' => 'company-other', 'user_uuid' => 'user-outsider', 'status' => 'active']);
1094
+
1095
+ $controller = new InspectionFormController();
1096
+ $form = fleetOpsInspectionControllerForm();
1097
+ $mint = fn (array $input) => $controller->generateLink(fleetOpsInspectionControllerInternalRequest('POST', $input), $form->public_id);
1098
+
1099
+ // Assigned to someone in the organisation, and emailed the link and PIN.
1100
+ $assigned = $mint(['assignee' => 'user_admin', 'pin_delivery' => 'email'])->getData(true);
1101
+ $link = InspectionLink::query()->where('public_id', $assigned['link']['id'])->first();
1102
+ expect($assigned['link']['assignee'])->toBe(['id' => 'user_admin', 'name' => 'Avery Admin'])
1103
+ ->and($assigned['link']['has_pin'])->toBeTrue()
1104
+ ->and($assigned['link']['pin'])->toMatch('/^\d{6}$/')
1105
+ ->and($link->pin)->toBe($assigned['link']['pin'])
1106
+ ->and(password_verify($assigned['link']['pin'], $link->pin_hash))->toBeTrue()
1107
+ ->and($assigned['link']['recipient'])->toBe(['name' => 'Avery Admin'])
1108
+ ->and($assigned['link']['can_send_pin'])->toBe(['email' => true, 'sms' => false])
1109
+ ->and($assigned['link']['pin_sent_via'])->toBe('email')
1110
+ ->and($assigned['pin_delivery'])->toBe(['sent' => true, 'via' => 'email', 'to' => 'a•••@example.com', 'error' => null])
1111
+ ->and($fakes->mail)->toHaveCount(1);
1112
+
1113
+ // A delivery that cannot happen is refused before anything is minted.
1114
+ $before = InspectionLink::query()->count();
1115
+ $nobody = $mint(['pin_delivery' => 'sms']);
1116
+ $noPhone = $mint(['assignee' => 'user_admin', 'pin_delivery' => 'sms']);
1117
+ expect($nobody->getStatusCode())->toBe(422)
1118
+ ->and($nobody->getData(true)['error'])->toContain('Assign the link to someone')
1119
+ ->and($noPhone->getStatusCode())->toBe(422)
1120
+ ->and($noPhone->getData(true)['error'])->toBe('Avery Admin has no phone number to text the PIN to.')
1121
+ ->and(InspectionLink::query()->count())->toBe($before);
1122
+
1123
+ // Not sent at all: the PIN comes back to be shared by hand.
1124
+ $byHand = $mint(['driver' => 'driver_one'])->getData(true);
1125
+ expect($byHand['pin_delivery'])->toBeNull()
1126
+ ->and($byHand['link']['pin'])->toMatch('/^\d{6}$/')
1127
+ ->and($byHand['link']['recipient'])->toBe(['name' => 'Dana Driver'])
1128
+ ->and($byHand['link']['can_send_pin'])->toBe(['email' => true, 'sms' => true]);
1129
+
1130
+ // Nobody outside the organisation can be assigned.
1131
+ expect(fn () => $mint(['assignee' => 'user_outsider']))->toThrow(ModelNotFoundException::class);
1132
+
1133
+ // Sent again from the link list.
1134
+ $send = fn (InspectionLink $to, string $via) => $controller->sendPin(fleetOpsInspectionControllerInternalRequest('POST', ['via' => $via]), $form->public_id, $to->public_id);
1135
+ $driverLink = InspectionLink::query()->where('public_id', $byHand['link']['id'])->first();
1136
+ $resent = $send($driverLink, 'sms')->getData(true);
1137
+ expect($resent['status'])->toBe('ok')
1138
+ ->and($resent['message'])->toBe('PIN sent.')
1139
+ ->and($resent['pin_delivery']['to'])->toBe('•••1111')
1140
+ ->and($resent['link']['pin_sent_via'])->toBe('sms');
1141
+
1142
+ $fakes->mailFails = new RuntimeException('SMTP refused');
1143
+ $failed = $send($driverLink, 'email')->getData(true);
1144
+ expect($failed['status'])->toBe('error')
1145
+ ->and($failed['message'])->toBe('The PIN could not be sent: SMTP refused');
1146
+
1147
+ // Nobody who can receive it, or a link no longer in use, is refused.
1148
+ expect($send($link, 'sms')->getStatusCode())->toBe(422);
1149
+ $link->revoke();
1150
+ $revoked = $send($link->fresh(), 'email');
1151
+ expect($revoked->getStatusCode())->toBe(422)
1152
+ ->and($revoked->getData(true)['error'])->toBe('Only an active link can have its PIN sent.');
1153
+ });
1154
+
1155
+ test('internal inspection form controller lists and revokes the links minted for a form', function () {
1156
+ $connection = fleetOpsInspectionControllerDatabase();
1157
+ Carbon::setTestNow('2026-09-09 12:00:00');
1158
+ $controller = new InspectionFormController();
1159
+ $form = fleetOpsInspectionControllerForm();
1160
+ $other = fleetOpsInspectionControllerForm(['name' => 'Another form']);
1161
+
1162
+ $active = fleetOpsInspectionControllerLink($form, 'active-token');
1163
+ $expired = fleetOpsInspectionControllerLink($form, 'expired-token', ['expires_at' => '2026-09-09 11:00:00']);
1164
+ $used = fleetOpsInspectionControllerLink($form, 'used-token', ['used_at' => '2026-09-09 10:00:00']);
1165
+ fleetOpsInspectionControllerLink($other, 'other-token');
1166
+ foreach ([[$active, '2026-09-09 09:00:00'], [$expired, '2026-09-09 08:00:00'], [$used, '2026-09-09 07:00:00']] as [$made, $at]) {
1167
+ $connection->table('inspection_links')->where('uuid', $made->uuid)->update(['created_at' => $at]);
1168
+ }
1169
+
1170
+ // Newest first, only this form's, each saying whether it still works.
1171
+ $listed = $controller->links(fleetOpsInspectionControllerInternalRequest('GET', ['limit' => 10]), $form->public_id)->getData(true)['links'];
1172
+ expect(array_column($listed, 'id'))->toBe([$active->public_id, $expired->public_id, $used->public_id])
1173
+ ->and(array_column($listed, 'state'))->toBe(['active', 'expired', 'used'])
1174
+ ->and($controller->links(fleetOpsInspectionControllerInternalRequest('GET', ['limit' => 1]), $form->public_id)->getData(true)['links'])->toHaveCount(1);
1175
+
1176
+ // Revoked by its public id, or by the numeric id an older list handed out.
1177
+ $revoked = $controller->revokeLink(Request::create('/', 'DELETE'), $form->public_id, $active->public_id)->getData(true);
1178
+ expect($revoked['status'])->toBe('ok')
1179
+ ->and($revoked['link']['state'])->toBe('revoked')
1180
+ ->and($active->fresh()->status)->toBe('revoked');
1181
+
1182
+ $numericId = (string) $connection->table('inspection_links')->where('uuid', $expired->uuid)->value('id');
1183
+ $byNumber = $controller->revokeLink(Request::create('/', 'DELETE'), $form->public_id, $numericId)->getData(true);
1184
+ expect($byNumber['link']['id'])->toBe($expired->public_id)
1185
+ ->and($expired->fresh()->status)->toBe('revoked');
1186
+
1187
+ expect(fn () => $controller->revokeLink(Request::create('/', 'DELETE'), $form->public_id, 'inspection_link_missing'))->toThrow(ModelNotFoundException::class);
1188
+ });
1189
+
1190
+ test('public inspection link asks for its pin, counts wrong ones, and locks', function () {
1191
+ fleetOpsInspectionControllerDatabase();
1192
+ $controller = new PublicInspectionController();
1193
+ $form = fleetOpsInspectionControllerForm();
1194
+ $link = fleetOpsInspectionControllerLink($form, 'pin-token', ['assignee_uuid' => 'user-admin']);
1195
+ $link->setPin('135790');
1196
+ $link->save();
1197
+
1198
+ $show = fn (array $server = [], array $query = []) => $controller->show(
1199
+ Request::create('/public/inspections/forms/x', 'GET', array_merge(['token' => 'pin-token'], $query), [], [], $server),
1200
+ $form->public_id
1201
+ );
1202
+
1203
+ $missing = fleetOpsInspectionControllerRefusal(fn () => $show());
1204
+ expect($missing->getStatusCode())->toBe(403)
1205
+ ->and($missing->getData(true))->toBe(['error' => 'Enter the PIN you were given with this link.', 'pin_required' => true]);
1206
+
1207
+ $wrong = fleetOpsInspectionControllerRefusal(fn () => $show(['HTTP_X_INSPECTION_PIN' => '000000']));
1208
+ expect($wrong->getStatusCode())->toBe(403)
1209
+ ->and($wrong->getData(true))->toBe(['error' => 'That PIN is not right.', 'pin_required' => true, 'attempts_left' => 4]);
1210
+
1211
+ // The right PIN, as a header or a field, opens the form and clears the count.
1212
+ $opened = $show(['HTTP_X_INSPECTION_PIN' => '135790'])->getData(true);
1213
+ expect($opened['identity']['assignee'])->toBe(['id' => 'user_admin', 'name' => 'Avery Admin'])
1214
+ ->and($link->fresh()->pin_attempts)->toBe(0)
1215
+ ->and($show([], ['pin' => '135-790'])->getStatusCode())->toBe(200);
1216
+
1217
+ // The fifth wrong PIN in a row locks the link, and it stays locked.
1218
+ foreach (range(1, InspectionLink::MAX_PIN_ATTEMPTS - 1) as $attempt) {
1219
+ fleetOpsInspectionControllerRefusal(fn () => $show(['HTTP_X_INSPECTION_PIN' => '000000']));
1220
+ }
1221
+ $locked = fleetOpsInspectionControllerRefusal(fn () => $show(['HTTP_X_INSPECTION_PIN' => '000000']));
1222
+ expect($locked->getStatusCode())->toBe(403)
1223
+ ->and($locked->getData(true)['locked'])->toBeTrue()
1224
+ ->and($link->fresh()->status)->toBe('locked');
1225
+
1226
+ $stillLocked = fleetOpsInspectionControllerRefusal(fn () => $show(['HTTP_X_INSPECTION_PIN' => '135790']));
1227
+ expect($stillLocked->getStatusCode())->toBe(403)
1228
+ ->and($stillLocked->getData(true)['locked'])->toBeTrue();
1229
+ });
1230
+
1231
+ test('public inspection link stores a photo through the link, and refuses what it cannot keep', function () {
1232
+ $connection = fleetOpsInspectionControllerDatabase();
1233
+ $root = fleetOpsInspectionControllerDisk();
1234
+ $controller = new PublicInspectionController();
1235
+ $form = fleetOpsInspectionControllerForm();
1236
+ $link = fleetOpsInspectionControllerLink($form, 'good-token');
1237
+ $upload = fn (array $input = []) => $controller->upload(
1238
+ Request::create('/public/inspections/forms/x/files', 'POST', array_merge(['token' => 'good-token'], $input), [], ['file' => fleetOpsInspectionControllerPhoto()]),
1239
+ $form->public_id
1240
+ );
1241
+
1242
+ // Named photo.php by the device, stored as the PNG its bytes say it is.
1243
+ $stored = $upload(['type' => 'inspection_signature'])->getData(true)['file'];
1244
+ $file = Fleetbase\Models\File::query()->where('public_id', $stored['id'])->first();
1245
+ expect($stored['filename'])->toBe('photo.php')
1246
+ ->and($stored['content_type'])->toBe('image/png')
1247
+ ->and($file->path)->toStartWith('inspections/links/' . $link->uuid . '/')
1248
+ ->and($file->path)->toEndWith('.png')
1249
+ ->and($stored['url'])->toBe('https://files.test/storage/' . $file->path)
1250
+ ->and(is_file($root . '/' . $file->path))->toBeTrue()
1251
+ ->and($file->company_uuid)->toBe('company-insp')
1252
+ ->and($file->uploader_uuid)->toBe('user-driver')
1253
+ ->and($file->type)->toBe('inspection_signature')
1254
+ ->and(data_get($file->meta, 'inspection_link_uuid'))->toBe($link->uuid);
1255
+
1256
+ // A disk that will not take the file says so, and nothing is recorded.
1257
+ $files = Fleetbase\Models\File::query()->count();
1258
+ app()->instance(Illuminate\Contracts\Filesystem\Factory::class, new class {
1259
+ public function disk($name = null)
1260
+ {
1261
+ return new class {
1262
+ public function putFileAs($path, $file, $name = null, $options = [])
1263
+ {
1264
+ return false;
1265
+ }
1266
+ };
1267
+ }
1268
+ });
1269
+ $failed = fleetOpsInspectionControllerRefusal(fn () => $upload());
1270
+ expect($failed->getStatusCode())->toBe(500)
1271
+ ->and($failed->getData(true)['error'])->toBe('This photo could not be stored.')
1272
+ ->and(Fleetbase\Models\File::query()->count())->toBe($files);
1273
+
1274
+ // One link can take only so many files.
1275
+ foreach (range(1, 40) as $n) {
1276
+ $connection->table('files')->insert(['uuid' => 'cap-' . $n, 'public_id' => 'file_cap' . $n, 'company_uuid' => 'company-insp', 'meta' => json_encode(['inspection_link_uuid' => $link->uuid])]);
1277
+ }
1278
+ $capped = fleetOpsInspectionControllerRefusal(fn () => $upload());
1279
+ expect($capped->getStatusCode())->toBe(422)
1280
+ ->and($capped->getData(true)['error'])->toBe('This inspection link has reached its upload limit.');
1281
+ });
1282
+
1283
+ test('public inspection link claims a single-use link once, and marks a reusable one used', function () {
1284
+ $connection = fleetOpsInspectionControllerDatabase();
1285
+ Carbon::setTestNow('2026-09-09 08:30:00');
1286
+ $controller = new PublicInspectionController();
1287
+ $form = fleetOpsInspectionControllerForm();
1288
+ InspectionFormSync::convertLegacyItems($form);
1289
+ $body = fn (string $token) => ['token' => $token, 'custom_field_values' => [
1290
+ ['custom_field' => 'brakes', 'value_type' => 'object', 'value' => ['passed' => true]],
1291
+ ['custom_field' => 'lights', 'value_type' => 'object', 'value' => ['passed' => true]],
1292
+ ]];
1293
+
1294
+ // A reusable link files, and is marked as used without being spent.
1295
+ $reusable = fleetOpsInspectionControllerLink($form, 'reuse-token', ['single_use' => false]);
1296
+ $controller->submit(Request::create('/x', 'POST', $body('reuse-token'), [], [], ['REMOTE_ADDR' => '198.51.100.4']), $form->public_id);
1297
+ expect($reusable->fresh()->used_at->toDateTimeString())->toBe('2026-09-09 08:30:00')
1298
+ ->and($reusable->fresh()->used_ip)->toBe('198.51.100.4')
1299
+ ->and($reusable->fresh()->isUsable())->toBeTrue();
1300
+
1301
+ // Two submits at once: the other took the link between this one's check
1302
+ // and its claim, so this one is refused and files nothing.
1303
+ $raced = fleetOpsInspectionControllerLink($form, 'race-token');
1304
+ $taken = false;
1305
+ InspectionLink::retrieved(function (InspectionLink $retrieved) use ($connection, $raced, &$taken) {
1306
+ if (!$taken && $retrieved->uuid === $raced->uuid) {
1307
+ $taken = true;
1308
+ $connection->table('inspection_links')->where('uuid', $raced->uuid)->update(['used_at' => '2026-09-09 08:29:59']);
1309
+ }
1310
+ });
1311
+ $before = InspectionSubmission::query()->count();
1312
+ $refused = fleetOpsInspectionControllerRefusal(fn () => $controller->submit(Request::create('/x', 'POST', $body('race-token')), $form->public_id));
1313
+ expect($refused->getStatusCode())->toBe(409)
1314
+ ->and($refused->getData(true)['error'])->toBe('This inspection link has already been used.')
1315
+ ->and(InspectionSubmission::query()->count())->toBe($before);
1316
+ });
1317
+
1318
+ test('public inspection routes answer in json whatever the client accepts', function () {
1319
+ // The platform's fetch service asks for anything, which on its own would
1320
+ // have a refused request redirected rather than answered in JSON.
1321
+ $untouched = Request::create('/public/inspections/forms/x/submit', 'POST', [], [], [], ['HTTP_ACCEPT' => '*/*']);
1322
+ $forced = Request::create('/public/inspections/forms/x/submit', 'POST', [], [], [], ['HTTP_ACCEPT' => '*/*']);
1323
+
1324
+ $accept = (new Fleetbase\FleetOps\Http\Middleware\ForceJsonResponse())->handle($forced, fn (Request $forwarded) => $forwarded->headers->get('Accept'));
1325
+
1326
+ expect($untouched->expectsJson())->toBeFalse()
1327
+ ->and($accept)->toBe('application/json')
1328
+ ->and($forced->expectsJson())->toBeTrue();
1329
+ });