@fleetbase/fleetops-engine 0.6.68 → 0.6.70
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.
- package/RELEASE.md +44 -9
- package/addon/components/admin/telematics-settings.hbs +143 -0
- package/addon/components/admin/telematics-settings.js +145 -0
- package/addon/components/contact/form.hbs +13 -2
- package/addon/components/customer/details.hbs +17 -17
- package/addon/components/customer/form.hbs +13 -2
- package/addon/components/customer/form.js +0 -56
- package/addon/components/driver/details.hbs +13 -19
- package/addon/components/driver/form.hbs +20 -35
- package/addon/components/driver/form.js +0 -58
- package/addon/components/fuel-integration/hub.hbs +1 -1
- package/addon/components/layout/fleet-ops-sidebar.js +37 -17
- package/addon/components/map/leaflet-live-map.hbs +4 -242
- package/addon/components/map/leaflet-live-map.js +2 -1
- package/addon/components/map/marker-card/driver.hbs +57 -0
- package/addon/components/map/marker-card/driver.js +3 -0
- package/addon/components/map/marker-card/vehicle.hbs +78 -0
- package/addon/components/map/marker-card/vehicle.js +3 -0
- package/addon/components/modals/driver-form.hbs +2 -2
- package/addon/components/modals/reset-profile-credentials.hbs +23 -0
- package/addon/components/modals/reset-profile-credentials.js +70 -0
- package/addon/components/profile-identity-hint.hbs +13 -0
- package/addon/components/profile-identity-hint.js +49 -0
- package/addon/components/telematic/hub.hbs +1 -1
- package/addon/components/widget/live-fleet.hbs +8 -40
- package/addon/controllers/analytics/reports/index.js +5 -0
- package/addon/controllers/connectivity/devices/index.js +4 -0
- package/addon/controllers/connectivity/fuel-providers/index.js +6 -4
- package/addon/controllers/connectivity/sensors/index.js +4 -0
- package/addon/controllers/connectivity/telematics/index.js +4 -0
- package/addon/controllers/maintenance/equipment/index.js +11 -4
- package/addon/controllers/maintenance/inspection-forms/index.js +2 -2
- package/addon/controllers/maintenance/inspection-submissions/index.js +8 -2
- package/addon/controllers/maintenance/maintenances/index.js +4 -0
- package/addon/controllers/maintenance/parts/index.js +11 -4
- package/addon/controllers/maintenance/schedules/index.js +17 -4
- package/addon/controllers/maintenance/work-orders/index.js +11 -4
- package/addon/controllers/management/contacts/index.js +4 -0
- package/addon/controllers/management/drivers/index.js +8 -0
- package/addon/controllers/management/fleets/index.js +4 -0
- package/addon/controllers/management/fuel-reports/index.js +4 -0
- package/addon/controllers/management/fuel-transactions/index.js +9 -7
- package/addon/controllers/management/issues/index.js +4 -0
- package/addon/controllers/management/places/index.js +4 -0
- package/addon/controllers/management/vehicles/index.js +4 -0
- package/addon/controllers/management/vendors/index.js +4 -0
- package/addon/controllers/operations/orders/index/details.js +30 -1
- package/addon/controllers/operations/orders/index.js +7 -0
- package/addon/controllers/operations/service-rates/index.js +3 -0
- package/addon/controllers/settings/index.js +13 -0
- package/addon/controllers/settings/telematics.js +113 -0
- package/addon/extension.js +42 -8
- package/addon/routes/analytics/index.js +14 -1
- package/addon/routes/analytics/reports/index/details.js +1 -1
- package/addon/routes/analytics/reports/index/edit.js +1 -1
- package/addon/routes/analytics/reports/index/new.js +14 -1
- package/addon/routes/analytics/reports.js +14 -1
- package/addon/routes/connectivity/devices/index/new.js +14 -1
- package/addon/routes/connectivity/devices.js +14 -1
- package/addon/routes/connectivity/events.js +14 -1
- package/addon/routes/connectivity/fuel-providers/details/transactions.js +12 -0
- package/addon/routes/connectivity/fuel-providers/details.js +9 -0
- package/addon/routes/connectivity/fuel-providers/edit.js +9 -0
- package/addon/routes/connectivity/fuel-providers/new.js +14 -1
- package/addon/routes/connectivity/fuel-providers.js +14 -1
- package/addon/routes/connectivity/sensors/index/new.js +14 -1
- package/addon/routes/connectivity/sensors.js +14 -1
- package/addon/routes/connectivity/telematics/new.js +14 -1
- package/addon/routes/connectivity/telematics.js +14 -1
- package/addon/routes/connectivity/tracking.js +14 -1
- package/addon/routes/maintenance/equipment/index/details.js +2 -2
- package/addon/routes/maintenance/equipment/index/edit.js +2 -2
- package/addon/routes/maintenance/equipment/index/new.js +14 -1
- package/addon/routes/maintenance/equipment.js +14 -1
- package/addon/routes/maintenance/index.js +14 -1
- package/addon/routes/maintenance/inspection-forms/index/details.js +10 -1
- package/addon/routes/maintenance/inspection-forms/index/edit.js +10 -1
- package/addon/routes/maintenance/inspection-forms/index/new.js +14 -1
- package/addon/routes/maintenance/inspection-forms.js +14 -1
- package/addon/routes/maintenance/inspection-submissions/index/details.js +10 -1
- package/addon/routes/maintenance/inspection-submissions/index/edit.js +10 -1
- package/addon/routes/maintenance/inspection-submissions/index/new.js +14 -1
- package/addon/routes/maintenance/inspection-submissions.js +14 -1
- package/addon/routes/maintenance/maintenances/index/details.js +2 -2
- package/addon/routes/maintenance/maintenances/index/edit.js +2 -2
- package/addon/routes/maintenance/maintenances/index/new.js +14 -1
- package/addon/routes/maintenance/maintenances.js +14 -1
- package/addon/routes/maintenance/parts/index/details.js +2 -2
- package/addon/routes/maintenance/parts/index/edit.js +2 -2
- package/addon/routes/maintenance/parts/index/new.js +14 -1
- package/addon/routes/maintenance/parts.js +14 -1
- package/addon/routes/maintenance/schedules/index/details.js +2 -2
- package/addon/routes/maintenance/schedules/index/edit.js +2 -2
- package/addon/routes/maintenance/schedules/index/new.js +15 -1
- package/addon/routes/maintenance/schedules.js +15 -1
- package/addon/routes/maintenance/work-orders/index/details.js +2 -2
- package/addon/routes/maintenance/work-orders/index/edit.js +2 -2
- package/addon/routes/maintenance/work-orders/index/new.js +14 -1
- package/addon/routes/maintenance/work-orders.js +14 -1
- package/addon/routes/management/fuel-transactions/index/details.js +9 -0
- package/addon/routes/management/fuel-transactions.js +14 -1
- package/addon/routes/management/index.js +13 -0
- package/addon/routes/management/trailers.js +16 -0
- package/addon/routes/management/vendors/integrated/details.js +14 -1
- package/addon/routes/management/vendors/integrated/edit.js +14 -1
- package/addon/routes/management/vendors/integrated/new.js +14 -1
- package/addon/routes/management/vendors/integrated.js +14 -1
- package/addon/routes/operations/orders.js +40 -1
- package/addon/routes/operations/routes/index/details.js +14 -1
- package/addon/routes/operations/routes/index/new.js +13 -0
- package/addon/routes/operations/routes.js +14 -1
- package/addon/routes/operations/service-rates/index/new.js +11 -0
- package/addon/routes/operations/service-rates.js +14 -1
- package/addon/routes/settings/avatars.js +14 -1
- package/addon/routes/settings/custom-fields.js +14 -1
- package/addon/routes/settings/map.js +14 -1
- package/addon/routes/settings/notifications.js +11 -0
- package/addon/routes/settings/orchestrator.js +1 -1
- package/addon/routes/settings/payments/index.js +1 -1
- package/addon/routes/settings/routing.js +14 -1
- package/addon/routes/settings/scheduling.js +14 -1
- package/addon/routes/settings/telematics.js +21 -0
- package/addon/routes/virtual.js +11 -0
- package/addon/routes.js +1 -0
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/settings/telematics.hbs +70 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/admin/telematics-settings.js +1 -0
- package/app/components/{modals/reset-customer-credentials.js → map/marker-card/driver.js} +1 -1
- package/app/components/map/marker-card/vehicle.js +1 -0
- package/app/components/modals/reset-profile-credentials.js +1 -0
- package/app/components/profile-identity-hint.js +1 -0
- package/app/controllers/settings/telematics.js +1 -0
- package/app/routes/settings/telematics.js +1 -0
- package/app/templates/settings/telematics.js +1 -0
- package/composer.json +2 -2
- package/docs/AFAQY.md +1 -1
- package/docs/TELEMATICS_QUEUES.md +1 -1
- package/docs/TELEMETRY_ARCHITECTURE.md +72 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/telemetry.php +12 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/migrations/2026_09_23_000001_add_retention_index_to_device_events.php +40 -0
- package/server/migrations/2026_09_23_000002_add_retention_index_to_positions.php +40 -0
- package/server/migrations/2026_09_23_000003_add_retention_index_to_telematic_sync_runs.php +40 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Auth/Schemas/FleetOps.php +104 -5
- package/server/src/Console/Commands/DrainTelematicInbox.php +3 -13
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- package/server/src/Console/Commands/ProcessOperationalAlerts.php +3 -3
- package/server/src/Console/Commands/PruneTelematicsData.php +318 -0
- package/server/src/Exceptions/ProfileIdentityConflictException.php +34 -0
- package/server/src/Http/Controllers/Api/v1/CustomerController.php +14 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +56 -39
- package/server/src/Http/Controllers/Api/v1/OrchestrationController.php +9 -0
- package/server/src/Http/Controllers/FleetOpsController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/AnalyticsController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/ContactController.php +16 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +54 -4
- package/server/src/Http/Controllers/Internal/v1/DeviceController.php +13 -0
- package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +204 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/FuelProviderConnectionController.php +13 -0
- package/server/src/Http/Controllers/Internal/v1/FuelProviderTransactionController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/FuelReportController.php +9 -3
- package/server/src/Http/Controllers/Internal/v1/InspectionFormController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +16 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/ManifestController.php +15 -0
- package/server/src/Http/Controllers/Internal/v1/MetricsController.php +11 -0
- package/server/src/Http/Controllers/Internal/v1/NavigatorController.php +42 -1
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +12 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +178 -27
- package/server/src/Http/Controllers/Internal/v1/PaymentController.php +13 -0
- package/server/src/Http/Controllers/Internal/v1/RadarController.php +19 -0
- package/server/src/Http/Controllers/Internal/v1/ServiceAreaController.php +9 -3
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +465 -14
- package/server/src/Http/Controllers/Internal/v1/TelematicController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/TrailerController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/VehicleController.php +21 -0
- package/server/src/Http/Controllers/Internal/v1/VendorController.php +17 -0
- package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +11 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +6 -2
- package/server/src/Http/Requests/Internal/CreateDriverRequest.php +9 -18
- package/server/src/Http/Resources/v1/Contact.php +3 -0
- package/server/src/Http/Resources/v1/Driver.php +3 -0
- package/server/src/Jobs/DispatchTelematicsRetentionJobs.php +70 -0
- package/server/src/Jobs/PruneTelematicsDataJob.php +74 -0
- package/server/src/Listeners/NotifyDriverOnShiftChange.php +3 -3
- package/server/src/Mail/CustomerCredentialsMail.php +1 -1
- package/server/src/Mail/DriverCredentialsMail.php +55 -0
- package/server/src/Models/Contact.php +55 -90
- package/server/src/Models/Sensor.php +3 -5
- package/server/src/Observers/ContactObserver.php +2 -22
- package/server/src/Observers/DriverObserver.php +5 -6
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +9 -1
- package/server/src/Providers/FleetOpsServiceProvider.php +18 -0
- package/server/src/Support/Ai/Capabilities/AbstractFleetOpsAICapability.php +62 -5
- package/server/src/Support/Ai/Capabilities/AssetStatusCapability.php +24 -5
- package/server/src/Support/Ai/Capabilities/CreateOrderPreviewCapability.php +6 -2
- package/server/src/Support/Ai/Capabilities/OrderInsightsCapability.php +40 -7
- package/server/src/Support/Ai/Capabilities/SearchResourcesCapability.php +86 -24
- package/server/src/Support/Ai/FleetOpsAiConsoleCommands.php +122 -0
- package/server/src/Support/Ai/Tools/CreateOrderTool.php +86 -0
- package/server/src/Support/Ai/Tools/ImportOrdersTool.php +53 -0
- package/server/src/Support/Ai/Tools/OptimizeOrderRouteTool.php +70 -0
- package/server/src/Support/Ai/Tools/SearchResourcesTool.php +73 -0
- package/server/src/Support/Analytics/LiveFleet.php +18 -2
- package/server/src/Support/Authorization.php +103 -0
- package/server/src/Support/Database/TableIndexes.php +66 -0
- package/server/src/Support/OrderTracker.php +2 -2
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Reporting/FleetOpsReportSchema.php +476 -116
- package/server/src/Support/Telematics/Retention/RetentionPolicy.php +300 -0
- package/server/src/Support/Telematics/Retention/TelemetryActivity.php +40 -0
- package/server/src/Support/Telematics/TelematicService.php +12 -2
- package/server/src/Support/Telematics/Telemetry/Ingestor.php +4 -2
- package/server/src/Tracking/TrackingIntelligenceService.php +1 -1
- package/server/src/Tracking/TrackingOptions.php +2 -2
- package/server/src/Traits/AuthorizesMethods.php +37 -0
- package/server/src/routes.php +14 -1
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- package/server/tests/AnalyticsRoutesTest.php +29 -2
- package/server/tests/ApiCustomerControllerContractsTest.php +21 -0
- package/server/tests/ApiDriverControllerContractsTest.php +102 -20
- package/server/tests/CommandContractsTest.php +4 -4
- package/server/tests/ControllerHelperContractsTest.php +3 -11
- package/server/tests/Feature/Http/AfaqyRealtimeIngestionTest.php +21 -1
- package/server/tests/Feature/Http/Api/DriverControllerAuthFlowsTest.php +36 -0
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +19 -6
- package/server/tests/Feature/Http/Api/ProfileOnlyUserConversionMigrationTest.php +201 -0
- package/server/tests/Feature/Http/Api/TelematicsRetentionIndexMigrationTest.php +115 -0
- package/server/tests/Feature/Http/Internal/ContactControllerEndpointsTest.php +6 -4
- package/server/tests/Feature/Http/Internal/CustomerControllerCredentialsTest.php +6 -2
- package/server/tests/Feature/Http/Internal/DriverControllerContractsTest.php +23 -0
- package/server/tests/Feature/Http/Internal/DriverControllerCreateRecordTest.php +42 -17
- package/server/tests/Feature/Http/Internal/DriverControllerExistingUserAdoptionTest.php +168 -277
- package/server/tests/Feature/Http/Internal/DriverControllerLoginManagementTest.php +716 -0
- package/server/tests/Feature/Http/Internal/OrderControllerContractsTest.php +10 -3
- package/server/tests/Feature/Http/Internal/OrderControllerImportFromFilesTest.php +18 -0
- package/server/tests/Feature/Http/Internal/OrderControllerTenantScopingTest.php +411 -0
- package/server/tests/Feature/Http/Internal/OrderControllerUpstreamNotFoundTest.php +8 -20
- package/server/tests/Feature/Http/Internal/SettingControllerHelpersTest.php +71 -0
- package/server/tests/Feature/Http/Internal/TelematicsStorageDeadlinesTest.php +119 -0
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/InternalImportExportControllerContractsTest.php +5 -8
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/NavigatorControllerContractsTest.php +36 -4
- package/server/tests/NotificationAndMailContractsTest.php +31 -0
- package/server/tests/ObserverContractsTest.php +28 -3
- package/server/tests/OrchestratorConsumableApiTest.php +42 -0
- package/server/tests/ProviderContractsTest.php +10 -0
- package/server/tests/ReportSchemaContractsTest.php +120 -6
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SettingControllerContractsTest.php +429 -18
- package/server/tests/SmallControllerContractsTest.php +8 -5
- package/server/tests/Support/PruneTelematicsDataProbe.php +55 -0
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- package/server/tests/Unit/Console/Commands/ProcessOperationalAlertsCommandTest.php +2 -2
- package/server/tests/Unit/Console/PruneTelematicsDataCommandTest.php +353 -0
- package/server/tests/Unit/Jobs/DispatchTelematicsRetentionJobsTest.php +114 -0
- package/server/tests/Unit/Jobs/PruneTelematicsDataJobTest.php +60 -0
- package/server/tests/Unit/Models/ContactCustomerUserTest.php +90 -14
- package/server/tests/Unit/Models/ContactTest.php +48 -5
- package/server/tests/Unit/Models/SensorTest.php +2 -2
- package/server/tests/Unit/Models/SmallModelSurfacesTest.php +31 -32
- package/server/tests/Unit/Observers/ObserverListenerSeamsTest.php +7 -2
- package/server/tests/Unit/Orchestration/VroomEngineTransportAndSettingsTest.php +20 -0
- package/server/tests/Unit/Providers/FleetOpsAiRegistrationTest.php +15 -2
- package/server/tests/Unit/Support/Ai/AssetStatusCapabilityQueriesTest.php +42 -0
- package/server/tests/Unit/Support/Ai/Capabilities/OrderInsightsCapabilityTest.php +19 -1
- package/server/tests/Unit/Support/Ai/Capabilities/SearchResourcesCapabilityTest.php +85 -1
- package/server/tests/Unit/Support/Ai/FleetOpsAiConsoleCommandsTest.php +31 -0
- package/server/tests/Unit/Support/Ai/Tools/FleetOpsAiToolsTest.php +246 -0
- package/server/tests/Unit/Support/AiCapabilityPermissionsTest.php +3 -3
- package/server/tests/Unit/Support/Analytics/LiveFleetTest.php +6 -0
- package/server/tests/Unit/Support/AuthorizationTest.php +142 -0
- package/server/tests/Unit/Support/MaintainableAndOperationalAlertsTest.php +12 -1
- package/server/tests/Unit/Support/MoreQuerySeamsTest.php +11 -2
- package/server/tests/Unit/Support/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/Telematics/Retention/RetentionPolicyTest.php +190 -0
- package/server/tests/Unit/Support/Telematics/Retention/TelemetryActivityTest.php +68 -0
- package/server/tests/Unit/Tracking/TrackingOptionsCompanySettingsTest.php +109 -0
- package/translations/en-us.yaml +85 -2
- package/translations/uk-ua.yaml +3475 -0
- package/addon/components/modals/reset-customer-credentials.hbs +0 -18
- package/addon/components/modals/reset-customer-credentials.js +0 -55
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Internal\v1\SettingController;
|
|
4
|
+
use Illuminate\Database\MySqlConnection;
|
|
5
|
+
use Illuminate\Database\QueryException;
|
|
6
|
+
use Illuminate\Support\Facades\DB;
|
|
7
|
+
|
|
8
|
+
class FleetOpsStorageDeadlineConnection extends MySqlConnection
|
|
9
|
+
{
|
|
10
|
+
public array $queries = [];
|
|
11
|
+
public array $responses = [];
|
|
12
|
+
public bool $maria = false;
|
|
13
|
+
|
|
14
|
+
public function getReadPdo()
|
|
15
|
+
{
|
|
16
|
+
return new class($this->maria) extends PDO {
|
|
17
|
+
public function __construct(private bool $maria)
|
|
18
|
+
{
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public function getAttribute(int $attribute): mixed
|
|
22
|
+
{
|
|
23
|
+
if ($attribute !== PDO::ATTR_SERVER_VERSION) {
|
|
24
|
+
throw new LogicException('Only the database server version should be read');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return $this->maria ? '10.11.8-MariaDB' : '8.0.40';
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public function select($query, $bindings = [], $useReadPdo = true)
|
|
33
|
+
{
|
|
34
|
+
$this->queries[] = [$query, $bindings];
|
|
35
|
+
$response = array_shift($this->responses) ?? [];
|
|
36
|
+
if ($response instanceof Throwable) {
|
|
37
|
+
throw $response;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return $response;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fleetopsStorageQueryError(int $code): QueryException
|
|
45
|
+
{
|
|
46
|
+
$error = new PDOException('Database query failed');
|
|
47
|
+
$error->errorInfo = ['HY000', $code, 'Database query failed'];
|
|
48
|
+
|
|
49
|
+
return new QueryException('mysql', 'select history', [], $error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beforeEach(function () {
|
|
53
|
+
$this->originalDb = app()->bound('db') ? app('db') : null;
|
|
54
|
+
$this->connection = new FleetOpsStorageDeadlineConnection(new PDO('sqlite::memory:'), '', '', ['driver' => 'mysql']);
|
|
55
|
+
$connection = $this->connection;
|
|
56
|
+
app()->instance('db', new class($connection) {
|
|
57
|
+
public function __construct(public $database)
|
|
58
|
+
{
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public function connection($name = null)
|
|
62
|
+
{
|
|
63
|
+
return $this->database;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public function __call($method, $args)
|
|
67
|
+
{
|
|
68
|
+
return $this->database->{$method}(...$args);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
DB::clearResolvedInstance('db');
|
|
72
|
+
$this->controller = new SettingController();
|
|
73
|
+
$this->invoke = fn ($method, ...$arguments) => (new ReflectionMethod(SettingController::class, $method))->invoke($this->controller, ...$arguments);
|
|
74
|
+
$this->scope = fn ($query) => $query->where('company_uuid', 'company-a');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(function () {
|
|
78
|
+
if ($this->originalDb) {
|
|
79
|
+
app()->instance('db', $this->originalDb);
|
|
80
|
+
} else {
|
|
81
|
+
app()->forgetInstance('db');
|
|
82
|
+
}
|
|
83
|
+
DB::clearResolvedInstance('db');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('storage scans enforce driver specific deadlines while preserving bindings', function (bool $maria) {
|
|
87
|
+
$this->connection->maria = $maria;
|
|
88
|
+
$this->connection->responses = [[(object) ['row_count' => '4', 'oldest' => '2026-01-01']], [(object) ['rows' => 4]]];
|
|
89
|
+
expect(($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toBe(['rows' => 4, 'oldest' => '2026-01-01']);
|
|
90
|
+
($this->invoke)('storageUsageSelect', 'select * from device_events where company_uuid = ?', ['company-a'], true);
|
|
91
|
+
expect($this->connection->queries[0][1])->toBe(['company-a'])
|
|
92
|
+
->and($this->connection->queries[0][0])->toStartWith($maria ? 'SET STATEMENT max_statement_time=1 FOR select' : 'SELECT /*+ MAX_EXECUTION_TIME(1000) */')
|
|
93
|
+
->and($this->connection->queries[1][0])->toStartWith($maria ? 'SET STATEMENT max_statement_time=1 FOR EXPLAIN select' : 'EXPLAIN SELECT /*+ MAX_EXECUTION_TIME(1000) */')
|
|
94
|
+
->and($this->connection->queries[1][1])->toBe(['company-a']);
|
|
95
|
+
})->with([false, true]);
|
|
96
|
+
|
|
97
|
+
test('timed out counts use a scoped query plan estimate instead of another history scan', function () {
|
|
98
|
+
$this->connection->responses = [fleetopsStorageQueryError(3024), [(object) ['rows' => 101, 'filtered' => 25]]];
|
|
99
|
+
expect(($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toBe(['rows' => 25, 'oldest' => null, 'rows_estimated' => true])
|
|
100
|
+
->and($this->connection->queries[1][0])->toStartWith('EXPLAIN SELECT')
|
|
101
|
+
->and($this->connection->queries[1][1])->toBe(['company-a']);
|
|
102
|
+
$this->connection->responses = [fleetopsStorageQueryError(1969), fleetopsStorageQueryError(1969)];
|
|
103
|
+
expect(($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toBe(['rows' => null, 'oldest' => null, 'rows_estimated' => true]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('storage queries suppress only timeout errors and retain real database failures', function () {
|
|
107
|
+
foreach (['tableUsage', 'tableOldest'] as $method) {
|
|
108
|
+
$this->connection->responses = [fleetopsStorageQueryError(1146)];
|
|
109
|
+
expect(fn () => ($this->invoke)($method, 'device_events', $this->scope, 'created_at'))->toThrow(QueryException::class);
|
|
110
|
+
}
|
|
111
|
+
$this->connection->responses = [fleetopsStorageQueryError(3024), fleetopsStorageQueryError(1146)];
|
|
112
|
+
expect(fn () => ($this->invoke)('tableUsage', 'device_events', $this->scope, 'created_at'))->toThrow(QueryException::class);
|
|
113
|
+
$this->connection->responses = [fleetopsStorageQueryError(3024)];
|
|
114
|
+
expect(($this->invoke)('tableOldest', 'device_events', $this->scope, 'created_at'))->toBeNull();
|
|
115
|
+
$this->connection->responses = [fleetopsStorageQueryError(1969)];
|
|
116
|
+
expect(($this->invoke)('storageTableStatistics', ['device_events']))->toBe([]);
|
|
117
|
+
$this->connection->responses = [fleetopsStorageQueryError(1146)];
|
|
118
|
+
expect(fn () => ($this->invoke)('storageTableStatistics', ['device_events']))->toThrow(QueryException::class);
|
|
119
|
+
});
|
|
@@ -122,11 +122,12 @@ function fleetopsInternalCustomer(): FleetOpsInternalCustomerContactFake
|
|
|
122
122
|
return $customer;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function fleetopsInternalCustomerUser(string $status = 'active'): FleetOpsInternalCustomerUserFake
|
|
125
|
+
function fleetopsInternalCustomerUser(string $status = 'active', string $type = 'customer'): FleetOpsInternalCustomerUserFake
|
|
126
126
|
{
|
|
127
127
|
$user = new FleetOpsInternalCustomerUserFake();
|
|
128
128
|
$user->setRawAttributes([
|
|
129
129
|
'uuid' => 'user-uuid',
|
|
130
|
+
'type' => $type,
|
|
130
131
|
'public_id' => 'user_public',
|
|
131
132
|
'name' => 'Ada Customer',
|
|
132
133
|
'email' => 'ada@example.test',
|
|
@@ -236,3 +237,33 @@ test('internal customer controller validates reset credential error branches', f
|
|
|
236
237
|
'password_confirmation' => 'one',
|
|
237
238
|
])))['error'])->toBe('Unable to reset customer credentials');
|
|
238
239
|
});
|
|
240
|
+
|
|
241
|
+
test('internal customer controller refuses login management for staff-linked customers', function () {
|
|
242
|
+
$staff = fleetopsInternalCustomerUser('active', 'user');
|
|
243
|
+
$controller = fleetopsInternalCustomerController($staff);
|
|
244
|
+
$message = 'This customer signs in with a team member account. Manage this login in IAM.';
|
|
245
|
+
$request = new Request(['customer' => 'customer_public']);
|
|
246
|
+
|
|
247
|
+
$responses = [
|
|
248
|
+
$controller->createPortalLogin($request),
|
|
249
|
+
$controller->sendCredentials($request),
|
|
250
|
+
$controller->deactivatePortalLogin($request),
|
|
251
|
+
$controller->reactivatePortalLogin($request),
|
|
252
|
+
$controller->resetCredentials(new Request([
|
|
253
|
+
'customer' => 'customer_public',
|
|
254
|
+
'password' => 'chosen-secret',
|
|
255
|
+
'password_confirmation' => 'chosen-secret',
|
|
256
|
+
])),
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
foreach ($responses as $response) {
|
|
260
|
+
expect($response->getStatusCode())->toBe(422)
|
|
261
|
+
->and(fleetopsInternalCustomerJson($response)['error'])->toBe($message);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The team member's account is never touched
|
|
265
|
+
expect($staff->passwords)->toBe([])
|
|
266
|
+
->and($staff->activations)->toBe(0)
|
|
267
|
+
->and($staff->deactivations)->toBe(0)
|
|
268
|
+
->and($controller->sentCredentials)->toBe([]);
|
|
269
|
+
});
|
|
@@ -269,14 +269,11 @@ test('internal fuel report controller syncs custom fields after save when provid
|
|
|
269
269
|
$controller = new FleetOpsInternalFuelReportControllerProbe();
|
|
270
270
|
$fuelReport = new FleetOpsInternalFuelReportAfterSaveFake();
|
|
271
271
|
|
|
272
|
-
$
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
],
|
|
278
|
-
]), $fuelReport);
|
|
279
|
-
$controller->afterSave(new Request(['fuel_report' => ['custom_field_values' => []]]), $fuelReport);
|
|
272
|
+
$input = ['custom_field_values' => [['key' => 'receipt_number', 'value' => 'R-100']]];
|
|
273
|
+
$controller->afterSave(new Request(['fuelReport' => $input]), $fuelReport, $input);
|
|
274
|
+
$controller->afterSave(new Request(), $fuelReport, ['custom_field_values' => []]);
|
|
275
|
+
$controller->afterSave(new Request(), $fuelReport, ['custom_field_values' => 'invalid']);
|
|
276
|
+
$controller->afterSave(new Request(), $fuelReport);
|
|
280
277
|
|
|
281
278
|
expect($fuelReport->synced)->toBe([
|
|
282
279
|
[
|
|
@@ -130,14 +130,51 @@ class FleetOpsContactSyncUserFake extends Fleetbase\Models\User
|
|
|
130
130
|
return true;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
public function save(array $options = []): bool
|
|
134
|
+
{
|
|
135
|
+
$this->updates[] = $this->getDirty();
|
|
136
|
+
$this->syncOriginal();
|
|
137
|
+
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
public function delete()
|
|
134
142
|
{
|
|
135
143
|
$this->deleted = true;
|
|
144
|
+
$this->setAttribute('deleted_at', '2026-01-01 00:00:00');
|
|
136
145
|
|
|
137
146
|
return true;
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
function fleetopsModelAccessorsUseProfileAccountTables(): void
|
|
151
|
+
{
|
|
152
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
153
|
+
$schema = $connection->getSchemaBuilder();
|
|
154
|
+
$schema->create('users', function ($table) {
|
|
155
|
+
$table->string('uuid')->nullable();
|
|
156
|
+
$table->string('type')->nullable();
|
|
157
|
+
$table->string('email')->nullable();
|
|
158
|
+
$table->string('phone')->nullable();
|
|
159
|
+
$table->timestamp('deleted_at')->nullable();
|
|
160
|
+
});
|
|
161
|
+
foreach (['drivers', 'contacts'] as $profileTable) {
|
|
162
|
+
$schema->create($profileTable, function ($table) {
|
|
163
|
+
$table->string('uuid')->nullable();
|
|
164
|
+
$table->string('user_uuid')->nullable();
|
|
165
|
+
$table->string('company_uuid')->nullable();
|
|
166
|
+
$table->timestamp('deleted_at')->nullable();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
$resolver = new ConnectionResolver([
|
|
171
|
+
'default' => $connection,
|
|
172
|
+
'mysql' => $connection,
|
|
173
|
+
]);
|
|
174
|
+
$resolver->setDefaultConnection('mysql');
|
|
175
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
176
|
+
}
|
|
177
|
+
|
|
141
178
|
class FleetOpsContactAccessorFake extends Contact
|
|
142
179
|
{
|
|
143
180
|
public array $loadedMissing = [];
|
|
@@ -858,6 +895,8 @@ test('contact accessors imports notifications and customer identity helpers are
|
|
|
858
895
|
});
|
|
859
896
|
|
|
860
897
|
test('contact user sync lookup and deletion guards are stable', function () {
|
|
898
|
+
fleetopsModelAccessorsUseProfileAccountTables();
|
|
899
|
+
|
|
861
900
|
$user = new FleetOpsContactSyncUserFake();
|
|
862
901
|
$user->setRawAttributes([
|
|
863
902
|
'uuid' => 'user-uuid',
|
|
@@ -868,22 +907,25 @@ test('contact user sync lookup and deletion guards are stable', function () {
|
|
|
868
907
|
], true);
|
|
869
908
|
|
|
870
909
|
$contact = new FleetOpsContactAccessorFake();
|
|
910
|
+
$contact->exists = true;
|
|
871
911
|
$contact->fakeUser = $user;
|
|
872
912
|
$contact->setRawAttributes([
|
|
873
|
-
'uuid'
|
|
874
|
-
'
|
|
875
|
-
'
|
|
876
|
-
'
|
|
877
|
-
'
|
|
878
|
-
'
|
|
879
|
-
'
|
|
913
|
+
'uuid' => 'contact-uuid',
|
|
914
|
+
'company_uuid' => 'company-uuid',
|
|
915
|
+
'user_uuid' => 'not-a-uuid',
|
|
916
|
+
'type' => 'customer',
|
|
917
|
+
'name' => 'Old Name',
|
|
918
|
+
'email' => 'old@example.com',
|
|
919
|
+
'phone' => '+15550000000',
|
|
920
|
+
'timezone' => 'UTC',
|
|
880
921
|
], true);
|
|
881
922
|
|
|
882
923
|
$contact->name = 'New Name';
|
|
883
|
-
$contact->email = '
|
|
884
|
-
$contact->phone = '+
|
|
924
|
+
$contact->email = 'New@Example.com';
|
|
925
|
+
$contact->phone = '+1 555-111-2222';
|
|
885
926
|
$contact->timezone = 'Asia/Singapore';
|
|
886
927
|
|
|
928
|
+
// Proxy fields are normalized and pushed to the managed account
|
|
887
929
|
expect($contact->syncWithUser())->toBeTrue()
|
|
888
930
|
->and($user->updates[0])->toBe([
|
|
889
931
|
'name' => 'New Name',
|
|
@@ -895,26 +937,46 @@ test('contact user sync lookup and deletion guards are stable', function () {
|
|
|
895
937
|
->and($contact->hasUser())->toBeTrue()
|
|
896
938
|
->and($contact->doesntHaveUser())->toBeFalse();
|
|
897
939
|
|
|
898
|
-
|
|
899
|
-
$
|
|
900
|
-
$
|
|
940
|
+
// An unsaved contact's account was just resolved from these values
|
|
941
|
+
$newContact = new FleetOpsContactAccessorFake();
|
|
942
|
+
$newContact->fakeUser = $user;
|
|
943
|
+
$newContact->name = 'Brand New';
|
|
944
|
+
|
|
945
|
+
// A saved contact with no proxy changes has nothing to sync
|
|
946
|
+
$cleanContact = new FleetOpsContactAccessorFake();
|
|
947
|
+
$cleanContact->exists = true;
|
|
948
|
+
$cleanContact->fakeUser = $user;
|
|
949
|
+
$cleanContact->setRawAttributes(['name' => 'New Name', 'type' => 'customer'], true);
|
|
950
|
+
|
|
951
|
+
expect($newContact->syncWithUser())->toBeFalse()
|
|
952
|
+
->and($cleanContact->syncWithUser())->toBeFalse()
|
|
953
|
+
->and($user->updates)->toHaveCount(1);
|
|
954
|
+
|
|
955
|
+
// Deleting the only profile holding a managed account deletes the account
|
|
956
|
+
$deleteContact = new FleetOpsContactAccessorFake();
|
|
957
|
+
$deleteContact->type = 'customer';
|
|
958
|
+
$deleteContact->company_uuid = 'company-uuid';
|
|
959
|
+
$deleteContact->fakeUser = $user;
|
|
901
960
|
|
|
902
961
|
expect($deleteContact->deleteUser())->toBeTrue()
|
|
903
|
-
->and($user->deleted)->toBeTrue()
|
|
904
|
-
->and($deleteContact->loadedMissing)->toBe(['user']);
|
|
962
|
+
->and($user->deleted)->toBeTrue();
|
|
905
963
|
|
|
906
|
-
|
|
907
|
-
$
|
|
964
|
+
// A team member's account linked to the contact is never deleted
|
|
965
|
+
$staffUser = new FleetOpsContactSyncUserFake();
|
|
966
|
+
$staffUser->setRawAttributes(['uuid' => 'staff-uuid', 'type' => 'admin'], true);
|
|
908
967
|
|
|
909
|
-
$
|
|
910
|
-
$
|
|
911
|
-
$
|
|
968
|
+
$staffContact = new FleetOpsContactAccessorFake();
|
|
969
|
+
$staffContact->type = 'customer';
|
|
970
|
+
$staffContact->fakeUser = $staffUser;
|
|
912
971
|
|
|
913
972
|
$emptyContact = new FleetOpsContactAccessorFake();
|
|
973
|
+
$emptyContact->exists = true;
|
|
914
974
|
$emptyContact->fakeUser = null;
|
|
915
975
|
$emptyContact->setRawAttributes(['user_uuid' => 'not-a-uuid'], true);
|
|
976
|
+
$emptyContact->name = 'Changed';
|
|
916
977
|
|
|
917
|
-
expect($
|
|
978
|
+
expect($staffContact->deleteUser())->toBeFalse()
|
|
979
|
+
->and($staffUser->deleted)->toBeFalse()
|
|
918
980
|
->and($emptyContact->syncWithUser())->toBeFalse()
|
|
919
981
|
->and($emptyContact->getUser())->toBeNull()
|
|
920
982
|
->and($emptyContact->hasUser())->toBeFalse()
|
|
@@ -137,17 +137,24 @@ function fleetopsNavigatorCredential(string $key = 'flb_live_key'): ApiCredentia
|
|
|
137
137
|
return $credential;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
function fleetopsNavigatorSignedQuery(): array
|
|
141
|
+
{
|
|
142
|
+
$expires = time() + 600;
|
|
143
|
+
|
|
144
|
+
return ['expires' => $expires, 'signature' => NavigatorController::linkSignature($expires)];
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
test('navigator controller builds android and ios app link redirects', function () {
|
|
141
148
|
$company = fleetopsNavigatorCompany();
|
|
142
149
|
$controller = new FleetOpsNavigatorControllerProbe();
|
|
143
150
|
$controller->adminUser = fleetopsNavigatorUser($company);
|
|
144
151
|
$controller->apiCredential = fleetopsNavigatorCredential('flb_live_navigator');
|
|
145
152
|
|
|
146
|
-
$android = $controller->linkApp(Request::create('/navigator/link-app', 'GET',
|
|
153
|
+
$android = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery(), [], [], [
|
|
147
154
|
'HTTP_USER_AGENT' => 'Mozilla/5.0 Android',
|
|
148
155
|
]))->getData(true);
|
|
149
156
|
|
|
150
|
-
$ios = $controller->linkApp(Request::create('/navigator/link-app', 'GET',
|
|
157
|
+
$ios = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery(), [], [], [
|
|
151
158
|
'HTTP_USER_AGENT' => 'Mozilla/5.0 iPhone',
|
|
152
159
|
]))->getData(true);
|
|
153
160
|
|
|
@@ -169,11 +176,32 @@ test('navigator controller returns missing organization error when no admin comp
|
|
|
169
176
|
$controller = new FleetOpsNavigatorControllerProbe();
|
|
170
177
|
$controller->adminUser = fleetopsNavigatorUser(null);
|
|
171
178
|
|
|
172
|
-
$response = $controller->linkApp(
|
|
179
|
+
$response = $controller->linkApp(Request::create('/navigator/link-app', 'GET', fleetopsNavigatorSignedQuery()));
|
|
173
180
|
|
|
174
181
|
expect($response->getData(true))->toBe(['error' => 'Organization for linking not found.']);
|
|
175
182
|
});
|
|
176
183
|
|
|
184
|
+
test('navigator controller refuses unsigned, tampered and expired app links before touching credentials', function () {
|
|
185
|
+
$company = fleetopsNavigatorCompany();
|
|
186
|
+
$controller = new FleetOpsNavigatorControllerProbe();
|
|
187
|
+
$controller->adminUser = fleetopsNavigatorUser($company);
|
|
188
|
+
$controller->apiCredential = fleetopsNavigatorCredential('flb_live_navigator');
|
|
189
|
+
|
|
190
|
+
$expired = time() - 60;
|
|
191
|
+
$requests = [
|
|
192
|
+
'unsigned' => Request::create('/navigator/link-app', 'GET'),
|
|
193
|
+
'tampered' => Request::create('/navigator/link-app', 'GET', ['signature' => 'forged'] + fleetopsNavigatorSignedQuery()),
|
|
194
|
+
'expired' => Request::create('/navigator/link-app', 'GET', ['expires' => $expired, 'signature' => NavigatorController::linkSignature($expired)]),
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
foreach ($requests as $request) {
|
|
198
|
+
$response = $controller->linkApp($request);
|
|
199
|
+
expect($response->getData(true))->toBe(['error' => 'This Navigator link is invalid or has expired. Generate a new one from the console.']);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
expect($controller->credentialLookups)->toBe([]);
|
|
203
|
+
});
|
|
204
|
+
|
|
177
205
|
test('navigator controller exposes link url settings and current organization token lookup branches', function () {
|
|
178
206
|
$controller = new FleetOpsNavigatorControllerProbe();
|
|
179
207
|
$controller->apiCredential = fleetopsNavigatorCredential('flb_test_key');
|
|
@@ -194,7 +222,11 @@ test('navigator controller exposes link url settings and current organization to
|
|
|
194
222
|
]);
|
|
195
223
|
$missing = $controller->getCurrentOrganization($secretRequest);
|
|
196
224
|
|
|
197
|
-
|
|
225
|
+
parse_str((string) parse_url($linkUrl['linkUrl'], PHP_URL_QUERY), $linkQuery);
|
|
226
|
+
|
|
227
|
+
expect($linkUrl['linkUrl'])->toStartWith('http://localhost/int/v1/fleet-ops/navigator/link-app?')
|
|
228
|
+
->and($linkQuery['signature'])->toBe(NavigatorController::linkSignature((int) $linkQuery['expires']))
|
|
229
|
+
->and((int) $linkQuery['expires'])->toBeGreaterThan(time())
|
|
198
230
|
->and($settings)->toBe(['enabled' => true, 'invite_code_required' => false])
|
|
199
231
|
->and($organization)->toBeInstanceOf(Organization::class)
|
|
200
232
|
->and($controller->credentialLookups)->toContain(
|
|
@@ -675,3 +675,34 @@ test('maintenance schedule reminder mail exposes schedule context', function ()
|
|
|
675
675
|
'offsetDays' => 7,
|
|
676
676
|
]);
|
|
677
677
|
});
|
|
678
|
+
|
|
679
|
+
test('customer credentials mail reads the portal slug from the customer company regardless of session', function (?string $sessionCompany) {
|
|
680
|
+
app('config')->set('fleetbase.console.host', 'console.fleetbase.test');
|
|
681
|
+
app('config')->set('fleetbase.console.secure', true);
|
|
682
|
+
app('config')->set('fleetbase.console.subdomain', null);
|
|
683
|
+
app('session.store')->put('company', $sessionCompany);
|
|
684
|
+
|
|
685
|
+
$connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
|
|
686
|
+
$resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
687
|
+
$resolver->setDefaultConnection('mysql');
|
|
688
|
+
Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
|
|
689
|
+
$connection->getSchemaBuilder()->create('settings', function ($blueprint) {
|
|
690
|
+
$blueprint->increments('id');
|
|
691
|
+
$blueprint->string('key')->nullable();
|
|
692
|
+
$blueprint->text('value')->nullable();
|
|
693
|
+
$blueprint->timestamps();
|
|
694
|
+
});
|
|
695
|
+
$connection->table('settings')->insert([
|
|
696
|
+
['key' => 'company.company-portal.customer-portal-config', 'value' => json_encode(['accessUrlSlug' => 'acme-portal'])],
|
|
697
|
+
['key' => 'company.other-company.customer-portal-config', 'value' => json_encode(['accessUrlSlug' => 'other-portal'])],
|
|
698
|
+
]);
|
|
699
|
+
|
|
700
|
+
$customer = new FleetOpsNotificationContactFake();
|
|
701
|
+
$customer->userForTest = new User();
|
|
702
|
+
$customer->userForTest->setRawAttributes(['email' => 'customer@example.test'], true);
|
|
703
|
+
$customer->setRawAttributes(['company_uuid' => 'company-portal'], true);
|
|
704
|
+
|
|
705
|
+
$content = fleetOpsNotificationWithEnvironment(fn () => (new CustomerCredentialsMail('plain-secret', $customer))->content());
|
|
706
|
+
|
|
707
|
+
expect($content->with['customerPortalUrl'])->toBe('https://console.fleetbase.test/acme-portal');
|
|
708
|
+
})->with([null, 'other-company']);
|
|
@@ -263,6 +263,7 @@ class FleetOpsDriverObserverUserFake extends User
|
|
|
263
263
|
public function delete()
|
|
264
264
|
{
|
|
265
265
|
$this->deleted = true;
|
|
266
|
+
$this->setAttribute('deleted_at', '2026-01-01 00:00:00');
|
|
266
267
|
|
|
267
268
|
return true;
|
|
268
269
|
}
|
|
@@ -453,7 +454,7 @@ class FleetOpsNotifyDriverOnShiftChangeProbe extends NotifyDriverOnShiftChange
|
|
|
453
454
|
return $this->schedule;
|
|
454
455
|
}
|
|
455
456
|
|
|
456
|
-
protected function getSchedulingSettings(): array
|
|
457
|
+
protected function getSchedulingSettings(?string $companyUuid): array
|
|
457
458
|
{
|
|
458
459
|
return $this->settings;
|
|
459
460
|
}
|
|
@@ -1149,14 +1150,30 @@ test('contact observer prevents changing existing customer contact type', functi
|
|
|
1149
1150
|
});
|
|
1150
1151
|
|
|
1151
1152
|
test('driver observer defaults location unassigns related records and deletes driver users', function () {
|
|
1153
|
+
// releaseForProfile looks for the account's remaining driver/contact profiles
|
|
1154
|
+
$connection = new Illuminate\Database\SQLiteConnection(new PDO('sqlite::memory:'));
|
|
1155
|
+
$resolver = new Illuminate\Database\ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
1156
|
+
$resolver->setDefaultConnection('mysql');
|
|
1157
|
+
Illuminate\Database\Eloquent\Model::setConnectionResolver($resolver);
|
|
1158
|
+
foreach (['drivers', 'contacts'] as $profileTable) {
|
|
1159
|
+
$connection->getSchemaBuilder()->create($profileTable, function ($table) {
|
|
1160
|
+
$table->string('uuid')->nullable();
|
|
1161
|
+
$table->string('user_uuid')->nullable();
|
|
1162
|
+
$table->string('company_uuid')->nullable();
|
|
1163
|
+
$table->timestamp('deleted_at')->nullable();
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1152
1167
|
$driver = new Driver();
|
|
1153
1168
|
$driver->setRawAttributes([
|
|
1154
|
-
'uuid'
|
|
1155
|
-
'user_uuid'
|
|
1169
|
+
'uuid' => 'driver-uuid',
|
|
1170
|
+
'user_uuid' => 'user-uuid',
|
|
1171
|
+
'company_uuid' => 'company-uuid',
|
|
1156
1172
|
], true);
|
|
1157
1173
|
|
|
1158
1174
|
$observer = new FleetOpsDriverObserverProbe();
|
|
1159
1175
|
$user = new FleetOpsDriverObserverUserFake();
|
|
1176
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'type' => 'driver'], true);
|
|
1160
1177
|
|
|
1161
1178
|
$observer->user = $user;
|
|
1162
1179
|
$observer->creating($driver);
|
|
@@ -1175,6 +1192,14 @@ test('driver observer defaults location unassigns related records and deletes dr
|
|
|
1175
1192
|
->and($observer->unassigned)->toBe(['driver-uuid'])
|
|
1176
1193
|
->and($user->deleted)->toBeTrue();
|
|
1177
1194
|
|
|
1195
|
+
// A team member's account linked to the driver is never deleted
|
|
1196
|
+
$staff = new FleetOpsDriverObserverUserFake();
|
|
1197
|
+
$staff->setRawAttributes(['uuid' => 'staff-uuid', 'type' => 'user'], true);
|
|
1198
|
+
$observer->user = $staff;
|
|
1199
|
+
$observer->deleted($driver);
|
|
1200
|
+
|
|
1201
|
+
expect($staff->deleted)->toBeFalse();
|
|
1202
|
+
|
|
1178
1203
|
$driverWithLocation = new Driver();
|
|
1179
1204
|
$location = new Fleetbase\LaravelMysqlSpatial\Types\Point(1.3, 103.8);
|
|
1180
1205
|
$driverWithLocation->location = $location;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
use Fleetbase\FleetOps\Http\Controllers\Api\v1\OrchestrationController;
|
|
4
4
|
use Fleetbase\FleetOps\Orchestration\OrchestrationEngineRegistry;
|
|
5
|
+
use Illuminate\Http\Exceptions\HttpResponseException;
|
|
6
|
+
use Illuminate\Http\Request;
|
|
5
7
|
|
|
6
8
|
function sanitize_orchestrator_payload(array $payload): array
|
|
7
9
|
{
|
|
@@ -72,3 +74,43 @@ test('consumable orchestrator routes are registered under the public api group',
|
|
|
72
74
|
expect($routes)->toContain("\$router->post('run', 'OrchestrationController@run');");
|
|
73
75
|
expect($routes)->toContain("\$router->post('commit', 'OrchestrationController@commit');");
|
|
74
76
|
});
|
|
77
|
+
|
|
78
|
+
test('public orchestration uses API authentication without inheriting console IAM middleware', function () {
|
|
79
|
+
$registry = new OrchestrationEngineRegistry();
|
|
80
|
+
$public = new OrchestrationController($registry);
|
|
81
|
+
$internal = new Fleetbase\FleetOps\Http\Controllers\Internal\v1\OrchestrationController($registry);
|
|
82
|
+
|
|
83
|
+
expect($public->getMiddleware())->toBe([]);
|
|
84
|
+
|
|
85
|
+
// An API-key request has no console IAM role. The internal workbench must
|
|
86
|
+
// still refuse this actor; its middleware must not leak onto the API class.
|
|
87
|
+
app('session.store')->flush();
|
|
88
|
+
$request = Request::create('/int/v1/fleet-ops/orchestrator/run', 'POST');
|
|
89
|
+
app()->instance('request', $request);
|
|
90
|
+
foreach (['run', 'commit'] as $method) {
|
|
91
|
+
$guard = collect($internal->getMiddleware())->first(fn ($entry) => in_array($method, $entry['options']['only'], true));
|
|
92
|
+
expect($guard)->not->toBeNull();
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
$guard['middleware']($request, fn () => throw new RuntimeException('Unauthorized request reached the workbench'));
|
|
96
|
+
test()->fail('The internal workbench must require a permitted user');
|
|
97
|
+
} catch (HttpResponseException $error) {
|
|
98
|
+
expect($error->getResponse()->getStatusCode())->toBe(401);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('public orchestration resolves tenant scope from the authenticated session, never request input', function () {
|
|
104
|
+
$controller = new OrchestrationController(new OrchestrationEngineRegistry());
|
|
105
|
+
$company = new ReflectionMethod($controller, 'companyUuid');
|
|
106
|
+
$company->setAccessible(true);
|
|
107
|
+
$request = Request::create('/v1/orchestrator/run', 'POST', ['company_uuid' => 'other-company']);
|
|
108
|
+
app()->instance('request', $request);
|
|
109
|
+
|
|
110
|
+
session(['company' => 'credential-company']);
|
|
111
|
+
expect($company->invoke($controller))->toBe('credential-company');
|
|
112
|
+
|
|
113
|
+
session(['company' => 'next-credential-company']);
|
|
114
|
+
expect($company->invoke($controller))->toBe('next-credential-company');
|
|
115
|
+
app('session.store')->flush();
|
|
116
|
+
});
|
|
@@ -115,6 +115,13 @@ class FleetOpsProviderContractsScheduledEventFake
|
|
|
115
115
|
return $this;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
public function everyFifteenMinutes(): self
|
|
119
|
+
{
|
|
120
|
+
$this->methods[] = ['everyFifteenMinutes'];
|
|
121
|
+
|
|
122
|
+
return $this;
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
public function daily(): self
|
|
119
126
|
{
|
|
120
127
|
$this->methods[] = ['daily'];
|
|
@@ -277,11 +284,14 @@ test('fleetops service provider executes package registration and boot wiring',
|
|
|
277
284
|
'fleetops:process-operational-alerts',
|
|
278
285
|
'fleetops:sync-telematics',
|
|
279
286
|
'fleetops:drain-telematic-inbox',
|
|
287
|
+
'fleetops:prune-telematics-data',
|
|
280
288
|
])
|
|
281
289
|
->and($provider->schedule?->commands['fleetops:dispatch-orders']->methods)->toContain(['everyMinute'], ['withoutOverlapping'], ['storeOutputInDb'])
|
|
282
290
|
->and($provider->schedule?->commands['fleetops:update-estimations']->methods)->toContain(['everyTenMinutes'], ['withoutOverlapping'])
|
|
283
291
|
->and($provider->schedule?->commands['fleetops:sync-telematics']->expiresAt)->toBe(2)
|
|
284
292
|
->and($provider->schedule?->commands['fleetops:drain-telematic-inbox']->expiresAt)->toBe(2)
|
|
293
|
+
->and($provider->schedule?->commands['fleetops:prune-telematics-data']->methods)->toContain(['everyFifteenMinutes'], ['withoutOverlapping'], ['storeOutputInDb'])
|
|
294
|
+
->and($provider->schedule?->commands['fleetops:prune-telematics-data']->expiresAt)->toBe(14)
|
|
285
295
|
->and($orchestrationRegistry->has('vroom'))->toBeTrue()
|
|
286
296
|
->and($orchestrationRegistry->has('greedy'))->toBeTrue()
|
|
287
297
|
->and($orchestrationRegistry->has('capacity'))->toBeTrue()
|