@fleetbase/fleetops-engine 0.6.67 → 0.6.69

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 (115) hide show
  1. package/RELEASE.md +11 -15
  2. package/addon/components/contact/form.hbs +13 -2
  3. package/addon/components/customer/details.hbs +17 -17
  4. package/addon/components/customer/form.hbs +13 -2
  5. package/addon/components/customer/form.js +0 -56
  6. package/addon/components/driver/details.hbs +13 -19
  7. package/addon/components/driver/form.hbs +20 -35
  8. package/addon/components/driver/form.js +0 -58
  9. package/addon/components/fuel-integration/form.hbs +2 -2
  10. package/addon/components/fuel-integration/form.js +8 -0
  11. package/addon/components/fuel-integration/hub.js +1 -1
  12. package/addon/components/modals/driver-form.hbs +2 -2
  13. package/addon/components/modals/reset-profile-credentials.hbs +23 -0
  14. package/addon/components/modals/reset-profile-credentials.js +70 -0
  15. package/addon/components/profile-identity-hint.hbs +13 -0
  16. package/addon/components/profile-identity-hint.js +49 -0
  17. package/addon/controllers/management/drivers/index.js +4 -0
  18. package/addon/extension.js +4 -2
  19. package/addon/services/contact-actions.js +50 -89
  20. package/addon/services/driver-actions.js +98 -0
  21. package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
  22. package/addon/utils/fuel-transaction.js +2 -0
  23. package/addon/utils/profile-login-actions.js +172 -0
  24. package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
  25. package/app/components/profile-identity-hint.js +1 -0
  26. package/composer.json +1 -1
  27. package/docs/SAFEE.md +5 -0
  28. package/docs/TELEMATICS_QUEUES.md +59 -8
  29. package/extension.json +1 -1
  30. package/package.json +3 -3
  31. package/scripts/pest-bootstrap.php +12 -0
  32. package/server/config/fuel-providers.php +53 -0
  33. package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
  34. package/server/resources/views/mail/driver-credentials.blade.php +21 -0
  35. package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
  36. package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
  37. package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
  38. package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
  39. package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
  40. package/server/src/Http/Controllers/Internal/v1/ContactController.php +5 -17
  41. package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
  42. package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
  43. package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
  44. package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
  45. package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
  46. package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
  47. package/server/src/Http/Resources/v1/Contact.php +3 -0
  48. package/server/src/Http/Resources/v1/Driver.php +3 -0
  49. package/server/src/Jobs/PollTelematicTelemetry.php +36 -0
  50. package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
  51. package/server/src/Mail/DriverCredentialsMail.php +55 -0
  52. package/server/src/Models/Contact.php +55 -90
  53. package/server/src/Models/Sensor.php +3 -5
  54. package/server/src/Observers/ContactObserver.php +2 -22
  55. package/server/src/Observers/DriverObserver.php +5 -6
  56. package/server/src/Providers/FleetOpsServiceProvider.php +15 -0
  57. package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
  58. package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
  59. package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
  60. package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
  61. package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
  62. package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
  63. package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
  64. package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
  65. package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
  66. package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
  67. package/server/src/Support/FuelProviders/Providers/SascoFuelProvider.php +272 -0
  68. package/server/src/Support/ProfileAccountManager.php +405 -0
  69. package/server/src/Support/Telematics/TelematicService.php +6 -4
  70. package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
  71. package/server/src/routes.php +5 -0
  72. package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
  73. package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
  74. package/server/tests/ApiDriverControllerContractsTest.php +102 -20
  75. package/server/tests/CommandContractsTest.php +4 -4
  76. package/server/tests/ControllerHelperContractsTest.php +3 -11
  77. package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +5 -0
  78. package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
  79. package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
  80. package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
  81. package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
  82. package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
  83. package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
  84. package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
  85. package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
  86. package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
  87. package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
  88. package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
  89. package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
  90. package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
  91. package/server/tests/Feature/Http/ManualTelemetryPollingTest.php +88 -4
  92. package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
  93. package/server/tests/ModelAccessorContractsTest.php +82 -20
  94. package/server/tests/ObserverContractsTest.php +27 -2
  95. package/server/tests/RequestContractsTest.php +19 -11
  96. package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
  97. package/server/tests/TelematicControllerContractsTest.php +22 -4
  98. package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
  99. package/server/tests/Unit/Models/ContactTest.php +48 -5
  100. package/server/tests/Unit/Models/SensorTest.php +2 -2
  101. package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
  102. package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
  103. package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
  104. package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
  105. package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
  106. package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
  107. package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
  108. package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
  109. package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
  110. package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
  111. package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
  112. package/translations/en-us.yaml +37 -2
  113. package/translations/uk-ua.yaml +3475 -0
  114. package/addon/components/modals/reset-customer-credentials.hbs +0 -18
  115. package/addon/components/modals/reset-customer-credentials.js +0 -55
@@ -0,0 +1,246 @@
1
+ <?php
2
+
3
+ use Fleetbase\Ai\Models\AiTask;
4
+ use Fleetbase\Ai\Support\AiAudience;
5
+ use Fleetbase\Ai\Support\AiToolContext;
6
+ use Fleetbase\FleetOps\Models\OrderConfig;
7
+ use Fleetbase\FleetOps\Support\Ai\Tools\CreateOrderTool;
8
+ use Fleetbase\FleetOps\Support\Ai\Tools\SearchResourcesTool;
9
+
10
+ class FleetOpsCreateOrderToolProbe extends CreateOrderTool
11
+ {
12
+ public array $resolvedPlaces = [];
13
+
14
+ public function exposeBuildDraft(AiTask $task, array $input = []): array
15
+ {
16
+ return $this->buildDraft($task, $input);
17
+ }
18
+
19
+ protected function can(string $permission): bool
20
+ {
21
+ return true;
22
+ }
23
+
24
+ protected function draftFromPrompt(string $prompt): array
25
+ {
26
+ return array_filter([
27
+ 'order_config_uuid' => 'order-config-uuid',
28
+ 'type' => 'transport',
29
+ 'payload' => $prompt !== '' ? ['pickup_query' => 'Parsed from prompt'] : [],
30
+ 'dispatched' => false,
31
+ ], fn ($value) => $value !== []) + ['payload' => []];
32
+ }
33
+
34
+ protected function resolvePlace(?string $query): ?array
35
+ {
36
+ return $this->resolvedPlaces[$query] ?? null;
37
+ }
38
+
39
+ protected function resolveOrderConfig(array $draft): ?OrderConfig
40
+ {
41
+ $config = new OrderConfig();
42
+ $config->setRawAttributes(['uuid' => 'order-config-uuid', 'key' => 'transport', 'name' => 'Transport'], true);
43
+
44
+ return $config;
45
+ }
46
+
47
+ protected function resolveDriver(array $draft): ?Fleetbase\FleetOps\Models\Driver
48
+ {
49
+ return null;
50
+ }
51
+
52
+ protected function resolveVehicle(array $draft): ?Fleetbase\FleetOps\Models\Vehicle
53
+ {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ class FleetOpsSearchResourcesToolProbe extends SearchResourcesTool
59
+ {
60
+ public array $searched = [];
61
+
62
+ protected function searchAll(array $terms, ?array $types = null): array
63
+ {
64
+ $this->searched[] = [$terms, $types];
65
+
66
+ return ['query_terms' => $terms, 'results' => []];
67
+ }
68
+ }
69
+
70
+ test('create order tool prepares one draft preview per call from structured fields without parsing the prompt', function () {
71
+ $tool = new FleetOpsCreateOrderToolProbe();
72
+ $task = new AiTask(['prompt' => 'Create some dummy orders with made up data', 'metadata' => []]);
73
+ $context = new AiToolContext($task, new AiAudience(false, ['fleet-ops create order']));
74
+
75
+ $first = $tool->invoke($task, [
76
+ 'pickup' => 'Kabelweg 57, Amsterdam',
77
+ 'dropoff' => 'Danzigerkade 15, Amsterdam',
78
+ 'scheduled_at' => '2026-09-16T09:30:00+02:00',
79
+ 'driver' => ' ',
80
+ 'notes' => 'Test order',
81
+ ], $context);
82
+ $second = $tool->invoke($task, ['pickup' => 'Diemerhof 32, Diemen', 'dropoff' => 'Herikerbergweg 238, Amsterdam', 'dispatch' => true], $context);
83
+
84
+ expect($first['preview_id'])->toBe('preview-1')
85
+ ->and($second['preview_id'])->toBe('preview-2')
86
+ ->and($first['status'])->toBe('draft_shown_to_user')
87
+ ->and($first['message'])->toContain('does not exist until the user reviews the card')
88
+ ->and($context->actionPreviews)->toHaveCount(2)
89
+ ->and($context->actionPreviews[0]['draft']['payload']['pickup_query'])->toBe('Kabelweg 57, Amsterdam')
90
+ ->and($context->actionPreviews[0]['draft']['scheduled_at'])->toBe('2026-09-16T09:30:00+02:00')
91
+ ->and($context->actionPreviews[0]['draft']['notes'])->toBe('Test order')
92
+ ->and($context->actionPreviews[0]['draft'])->not->toHaveKey('driver_query')
93
+ ->and($context->actionPreviews[0]['draft']['dispatched'])->toBeFalse()
94
+ ->and($context->actionPreviews[1]['draft']['payload']['pickup_query'])->toBe('Diemerhof 32, Diemen')
95
+ ->and($context->actionPreviews[1]['draft']['dispatched'])->toBeTrue()
96
+ ->and(json_encode($context->actionPreviews))->not->toContain('Parsed from prompt')
97
+ ->and($tool->toolName())->toBe('propose_create_order')
98
+ ->and($tool->key())->toBe('fleet-ops.create_order')
99
+ ->and($tool->toolDescription())->toContain('NOT created by this tool')
100
+ ->and($tool->toolParameters()['required'])->toBe(['pickup', 'dropoff'])
101
+ ->and($tool->availableFor($context))->toBeTrue()
102
+ ->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, []))))->toBeFalse();
103
+ });
104
+
105
+ test('create order drafts refresh from the edited preview and still parse prompts in keyword mode', function () {
106
+ $tool = new FleetOpsCreateOrderToolProbe();
107
+ $task = new AiTask(['prompt' => 'create order', 'metadata' => ['action_previews' => [['draft' => ['notes' => 'first preview']]]]]);
108
+
109
+ $refreshed = $tool->exposeBuildDraft($task, ['existing_draft' => ['notes' => 'second preview'], 'draft' => ['dispatched' => true]]);
110
+ $keyword = $tool->exposeBuildDraft($task, ['dispatched' => false]);
111
+
112
+ expect($refreshed['notes'])->toBe('second preview')
113
+ ->and($refreshed['dispatched'])->toBeTrue()
114
+ ->and($refreshed['payload']['pickup_query'])->toBe('Parsed from prompt')
115
+ ->and($keyword['notes'])->toBe('first preview')
116
+ ->and($keyword)->not->toHaveKey('existing_draft');
117
+ });
118
+
119
+ test('search tool searches the requested phrase and identifiers for the chosen record types', function () {
120
+ $tool = new FleetOpsSearchResourcesToolProbe();
121
+ $task = new AiTask(['prompt' => 'where is Jane']);
122
+ $context = new AiToolContext($task, new AiAudience(false, ['fleet-ops see driver']));
123
+
124
+ $tool->invoke($task, ['query' => 'Jane Doe', 'types' => ['drivers', 'pets']], $context);
125
+ $tool->invoke($task, ['query' => 'order order_yhkejdnzgz'], $context);
126
+
127
+ expect($tool->searched[0])->toBe([['Jane Doe', 'Doe'], ['drivers']])
128
+ ->and($tool->searched[1])->toBe([['order order_yhkejdnzgz', 'order_yhkejdnzgz'], null])
129
+ ->and($tool->invoke($task, ['query' => ' x '], $context))->toBe(['error' => 'Provide an identifier or name to search for.'])
130
+ ->and($tool->toolName())->toBe('fleetops_search')
131
+ ->and($tool->key())->toBe('fleet-ops.search_resources')
132
+ ->and($tool->toolDescription())->toContain('particular record')
133
+ ->and($tool->toolParameters()['properties']['types']['items']['enum'])->toBe(SearchResourcesTool::TYPES)
134
+ ->and($tool->availableFor($context))->toBeTrue()
135
+ ->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, []))))->toBeFalse();
136
+ });
137
+
138
+ test('search tool narrows the real search to the requested types', function () {
139
+ $tool = new class extends SearchResourcesTool {
140
+ public array $called = [];
141
+
142
+ protected function orders(array $terms): array
143
+ {
144
+ $this->called[] = 'orders';
145
+
146
+ return [['id' => 'order_1']];
147
+ }
148
+
149
+ protected function drivers(array $terms): array
150
+ {
151
+ $this->called[] = 'drivers';
152
+
153
+ return [];
154
+ }
155
+ };
156
+
157
+ $result = $tool->invoke(new AiTask(['prompt' => '']), ['query' => 'ORDER-1', 'types' => ['orders']], new AiToolContext(new AiTask(), new AiAudience(true)));
158
+
159
+ expect($tool->called)->toBe(['orders'])
160
+ ->and($result['results'])->toBe(['orders' => [['id' => 'order_1']]]);
161
+ });
162
+
163
+ class FleetOpsOptimizeOrderRouteToolProbe extends Fleetbase\FleetOps\Support\Ai\Tools\OptimizeOrderRouteTool
164
+ {
165
+ public array $previewedPrompts = [];
166
+
167
+ public function preview(AiTask $task, array $input = []): array
168
+ {
169
+ $this->previewedPrompts[] = $task->prompt;
170
+
171
+ return [
172
+ 'action' => $this->key(),
173
+ 'ready' => true,
174
+ 'message' => 'Fleetbase AI prepared an optimized waypoint sequence. Review it before applying.',
175
+ 'missing_fields' => [],
176
+ 'fields' => [['label' => 'Order', 'value' => 'order_abc']],
177
+ ];
178
+ }
179
+ }
180
+
181
+ test('optimize route tool proposes a reviewable preview for the order the model named', function () {
182
+ $tool = new FleetOpsOptimizeOrderRouteToolProbe();
183
+ $task = new AiTask(['prompt' => 'can you optimise the route please', 'metadata' => []]);
184
+ $context = new AiToolContext($task, new AiAudience(false, ['fleet-ops optimize order', 'fleet-ops update-route-for order']));
185
+
186
+ $result = $tool->invoke($task, ['order' => 'order_abc'], $context);
187
+
188
+ expect($result['preview_id'])->toBe('preview-1')
189
+ ->and($result['ready'])->toBeTrue()
190
+ ->and($result['status'])->toBe('proposal_shown_to_user')
191
+ ->and($result['fields'])->toBe([['label' => 'Order', 'value' => 'order_abc']])
192
+ ->and($result['missing_fields'])->toBe([])
193
+ // The capability finds the order in the prompt text, so the tool hands it the id the model gave.
194
+ ->and($tool->previewedPrompts)->toBe(['order_abc'])
195
+ ->and($task->prompt)->toBe('can you optimise the route please')
196
+ ->and($context->actionPreviews)->toHaveCount(1)
197
+ ->and($tool->toolName())->toBe('propose_optimize_order_route')
198
+ ->and($tool->key())->toBe('fleet-ops.optimize_order_route')
199
+ ->and($tool->toolDescription())->toContain('NOT changed by this tool')
200
+ ->and($tool->toolParameters()['required'])->toBe(['order'])
201
+ ->and($tool->availableFor($context))->toBeTrue()
202
+ ->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, ['fleet-ops optimize order']))))->toBeFalse();
203
+ });
204
+
205
+ test('optimize route tool refuses to build a preview without an order', function () {
206
+ $tool = new FleetOpsOptimizeOrderRouteToolProbe();
207
+ $task = new AiTask(['prompt' => 'optimise a route', 'metadata' => []]);
208
+ $context = new AiToolContext($task, new AiAudience(false, ['fleet-ops optimize order', 'fleet-ops update-route-for order']));
209
+
210
+ expect($tool->invoke($task, ['order' => ' '], $context))->toBe([
211
+ 'error' => 'invalid_arguments',
212
+ 'message' => 'An order id is required to optimize a route.',
213
+ ])
214
+ ->and($tool->invoke($task, [], $context)['error'])->toBe('invalid_arguments')
215
+ ->and($context->actionPreviews)->toBe([])
216
+ ->and($tool->previewedPrompts)->toBe([]);
217
+ });
218
+
219
+ class FleetOpsImportOrdersToolProbe extends Fleetbase\FleetOps\Support\Ai\Tools\ImportOrdersTool
220
+ {
221
+ protected function can(string $permission): bool
222
+ {
223
+ return true;
224
+ }
225
+ }
226
+
227
+ test('import tool reports what an import needs and never claims to have imported anything', function () {
228
+ $tool = new FleetOpsImportOrdersToolProbe();
229
+ $task = new AiTask(['prompt' => 'can I import orders from a spreadsheet', 'metadata' => []]);
230
+ $context = new AiToolContext($task, new AiAudience(false, ['fleet-ops import order']));
231
+
232
+ $result = $tool->invoke($task, [], $context);
233
+
234
+ expect($result['accepted_sources'])->toBe(['xlsx', 'csv'])
235
+ ->and($result['minimum_columns'])->toContain('pickup address or pickup place')
236
+ ->and($result['can_import_here'])->toBeFalse()
237
+ ->and($result['message'])->toContain('will not process uploaded spreadsheets')
238
+ ->and($context->actionPreviews)->toBe([])
239
+ ->and($tool->toolName())->toBe('fleetops_import_requirements')
240
+ ->and($tool->key())->toBe('fleet-ops.import_orders_preview')
241
+ ->and($tool->toolDescription())->toContain('does NOT import anything')
242
+ ->and($tool->toolParameters()['properties'])->toBeInstanceOf(stdClass::class)
243
+ ->and(json_encode($tool->toolParameters()['properties']))->toBe('{}')
244
+ ->and($tool->availableFor($context))->toBeTrue()
245
+ ->and($tool->availableFor(new AiToolContext($task, new AiAudience(false, []))))->toBeFalse();
246
+ });
@@ -96,7 +96,7 @@ test('permission checks for non admin users delegate to the gate', function () {
96
96
  ->and(fn () => fleetopsAiPermissionsHelper('canAll')->invoke($capability, ['fleet-ops list order']))->toThrow(TypeError::class);
97
97
  });
98
98
 
99
- test('search terms extract identifiers and fall back to the raw prompt', function () {
99
+ test('search terms extract identifiers and never fall back to the raw prompt', function () {
100
100
  fleetopsAiPermissionsBoot();
101
101
  $capability = new OperationalQueryCapability();
102
102
  $searchTerms = fleetopsAiPermissionsHelper('searchTerms');
@@ -105,8 +105,8 @@ test('search terms extract identifiers and fall back to the raw prompt', functio
105
105
  expect($terms)->toContain('TRK-12345', 'atlas99')
106
106
  ->and($terms)->not->toContain('find', 'order');
107
107
 
108
- // Stop-word-only prompts fall back to the trimmed prompt
109
- expect($searchTerms->invoke($capability, 'find order'))->toBe(['find order']);
108
+ // Prompts without a record reference produce no search terms, so nothing is searched
109
+ expect($searchTerms->invoke($capability, 'find order'))->toBe([]);
110
110
  });
111
111
 
112
112
  test('where like any matches across columns and terms', function () {