@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.
- package/RELEASE.md +11 -15
- 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/form.hbs +2 -2
- package/addon/components/fuel-integration/form.js +8 -0
- package/addon/components/fuel-integration/hub.js +1 -1
- 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/controllers/management/drivers/index.js +4 -0
- package/addon/extension.js +4 -2
- package/addon/services/contact-actions.js +50 -89
- package/addon/services/driver-actions.js +98 -0
- package/addon/templates/connectivity/fuel-providers/details/index.hbs +1 -1
- package/addon/utils/fuel-transaction.js +2 -0
- package/addon/utils/profile-login-actions.js +172 -0
- package/app/components/modals/{reset-customer-credentials.js → reset-profile-credentials.js} +1 -1
- package/app/components/profile-identity-hint.js +1 -0
- package/composer.json +1 -1
- package/docs/SAFEE.md +5 -0
- package/docs/TELEMATICS_QUEUES.md +59 -8
- package/extension.json +1 -1
- package/package.json +3 -3
- package/scripts/pest-bootstrap.php +12 -0
- package/server/config/fuel-providers.php +53 -0
- package/server/migrations/2026_09_21_000001_convert_profile_only_users_to_managed_accounts.php +132 -0
- package/server/resources/views/mail/driver-credentials.blade.php +21 -0
- package/server/src/Console/Commands/FixCustomerCompanies.php +1 -1
- package/server/src/Console/Commands/FixDriverCompanies.php +1 -1
- 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/Internal/v1/ContactController.php +5 -17
- package/server/src/Http/Controllers/Internal/v1/CustomerController.php +40 -4
- package/server/src/Http/Controllers/Internal/v1/DriverController.php +182 -162
- package/server/src/Http/Controllers/Internal/v1/FleetOpsLookupController.php +47 -0
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +156 -27
- 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/PollTelematicTelemetry.php +36 -0
- package/server/src/Jobs/SyncTelematicDevicesJob.php +2 -2
- 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/Providers/FleetOpsServiceProvider.php +15 -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/FuelProviders/Providers/SascoFuelProvider.php +272 -0
- package/server/src/Support/ProfileAccountManager.php +405 -0
- package/server/src/Support/Telematics/TelematicService.php +6 -4
- package/server/src/Support/Telematics/Telemetry/Inbox.php +27 -9
- package/server/src/routes.php +5 -0
- package/server/tests/AiOperationalQueryCapabilityTest.php +1 -0
- 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 +5 -0
- 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/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/ManualTelemetryPollingTest.php +88 -4
- package/server/tests/InternalCustomerControllerContractsTest.php +32 -1
- package/server/tests/ModelAccessorContractsTest.php +82 -20
- package/server/tests/ObserverContractsTest.php +27 -2
- package/server/tests/RequestContractsTest.php +19 -11
- package/server/tests/SupportJobAndAiCoverageTest.php +3 -3
- package/server/tests/TelematicControllerContractsTest.php +22 -4
- 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/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/ProfileAccountManagerTest.php +643 -0
- package/server/tests/Unit/Support/SascoFuelProviderTest.php +338 -0
- package/translations/en-us.yaml +37 -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
|
@@ -39,6 +39,7 @@ use Fleetbase\Models\File;
|
|
|
39
39
|
use Fleetbase\Models\Type;
|
|
40
40
|
use Fleetbase\Support\Auth;
|
|
41
41
|
use Fleetbase\Support\TemplateString;
|
|
42
|
+
use Illuminate\Database\Eloquent\Builder;
|
|
42
43
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
43
44
|
use Illuminate\Database\QueryException;
|
|
44
45
|
use Illuminate\Http\Request;
|
|
@@ -341,7 +342,8 @@ class OrderController extends FleetOpsController
|
|
|
341
342
|
$info = Utils::lookupIp();
|
|
342
343
|
$disk = $request->input('disk', config('filesystems.default'));
|
|
343
344
|
$files = $request->input('files');
|
|
344
|
-
|
|
345
|
+
/** @var \Illuminate\Database\Eloquent\Collection<int, File> $files */
|
|
346
|
+
$files = $this->scopedToCompany(File::whereIn('uuid', $files))->get();
|
|
345
347
|
$country = $request->input('country', Utils::or($info, ['country_name', 'region'], 'Singapore'));
|
|
346
348
|
|
|
347
349
|
$validFileTypes = ['csv', 'tsv', 'xls', 'xlsx'];
|
|
@@ -513,7 +515,13 @@ class OrderController extends FleetOpsController
|
|
|
513
515
|
}
|
|
514
516
|
|
|
515
517
|
// Prepare Order UUID Collection
|
|
516
|
-
|
|
518
|
+
//
|
|
519
|
+
// Resolved through the company-scoped lookup so ids naming another
|
|
520
|
+
// organization's orders are dropped here rather than being assigned,
|
|
521
|
+
// counted in the response and queued for notification.
|
|
522
|
+
$orderUuids = $this->ordersByUuid(collect($data['ids'])->unique()->values()->all())
|
|
523
|
+
->pluck('uuid')
|
|
524
|
+
->values();
|
|
517
525
|
|
|
518
526
|
// Bulk Update Inside A Transaction
|
|
519
527
|
$this->runTransaction(function () use ($orderUuids, $driver): void {
|
|
@@ -545,8 +553,14 @@ class OrderController extends FleetOpsController
|
|
|
545
553
|
*/
|
|
546
554
|
public function cancel(CancelOrderRequest $request)
|
|
547
555
|
{
|
|
548
|
-
/** @var Order */
|
|
556
|
+
/** @var Order|null */
|
|
549
557
|
$order = $this->findOrderByUuid($request->input('order'));
|
|
558
|
+
if (!$order) {
|
|
559
|
+
// `exists:orders,uuid` on the form request is a global existence
|
|
560
|
+
// check, so a known uuid belonging to another organization reaches
|
|
561
|
+
// here and must be rejected rather than dereferenced.
|
|
562
|
+
return $this->errorResponse('No order found to cancel.');
|
|
563
|
+
}
|
|
550
564
|
|
|
551
565
|
$order->cancel();
|
|
552
566
|
|
|
@@ -599,9 +613,39 @@ class OrderController extends FleetOpsController
|
|
|
599
613
|
);
|
|
600
614
|
}
|
|
601
615
|
|
|
616
|
+
/**
|
|
617
|
+
* Constrain a tenant-owned lookup to the company the caller is acting for.
|
|
618
|
+
*
|
|
619
|
+
* The order-lifecycle actions on this controller receive their target as a
|
|
620
|
+
* caller-supplied identifier in the request body or query string rather than
|
|
621
|
+
* as a bound route parameter, so nothing upstream narrows these queries to
|
|
622
|
+
* the caller's tenant: `fleetbase.protected` only checks that the caller
|
|
623
|
+
* holds the named RBAC capability, never which company the record belongs to.
|
|
624
|
+
*
|
|
625
|
+
* A missing company session fails the query closed instead of letting it run
|
|
626
|
+
* unbounded across every tenant.
|
|
627
|
+
*/
|
|
628
|
+
protected function scopedToCompany(Builder $query): Builder
|
|
629
|
+
{
|
|
630
|
+
$companyUuid = $this->sessionCompany();
|
|
631
|
+
if (!$companyUuid) {
|
|
632
|
+
$query->whereRaw('1 = 0');
|
|
633
|
+
|
|
634
|
+
return $query;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return $query->where($query->getModel()->qualifyColumn('company_uuid'), $companyUuid);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* @return \Illuminate\Database\Eloquent\Collection<int, Order>
|
|
642
|
+
*/
|
|
602
643
|
protected function ordersByUuid(array $ids)
|
|
603
644
|
{
|
|
604
|
-
|
|
645
|
+
/** @var \Illuminate\Database\Eloquent\Collection<int, Order> $orders */
|
|
646
|
+
$orders = $this->scopedToCompany(Order::whereIn('uuid', $ids))->get();
|
|
647
|
+
|
|
648
|
+
return $orders;
|
|
605
649
|
}
|
|
606
650
|
|
|
607
651
|
protected function trackingStatusExists(?string $trackingNumberUuid, string $code): bool
|
|
@@ -611,7 +655,10 @@ class OrderController extends FleetOpsController
|
|
|
611
655
|
|
|
612
656
|
protected function findDriverByUuid(string $uuid): ?Driver
|
|
613
657
|
{
|
|
614
|
-
|
|
658
|
+
/** @var Driver|null $driver */
|
|
659
|
+
$driver = $this->scopedToCompany(Driver::whereUuid($uuid))->first();
|
|
660
|
+
|
|
661
|
+
return $driver;
|
|
615
662
|
}
|
|
616
663
|
|
|
617
664
|
protected function driverDisplayName(Driver $driver): string
|
|
@@ -630,17 +677,47 @@ class OrderController extends FleetOpsController
|
|
|
630
677
|
|
|
631
678
|
protected function findOrderByUuid(string $uuid): ?Order
|
|
632
679
|
{
|
|
633
|
-
|
|
680
|
+
/** @var Order|null $order */
|
|
681
|
+
$order = $this->scopedToCompany(Order::where('uuid', $uuid))->first();
|
|
682
|
+
|
|
683
|
+
return $order;
|
|
634
684
|
}
|
|
635
685
|
|
|
636
|
-
|
|
686
|
+
/**
|
|
687
|
+
* Resolve an order by uuid or public_id, constrained to the caller's company.
|
|
688
|
+
*
|
|
689
|
+
* The identifier match is grouped so the company constraint applies to both
|
|
690
|
+
* arms — ungrouped it would read as `uuid = ? OR (public_id = ? AND
|
|
691
|
+
* company_uuid = ?)` and still resolve another organization's orders.
|
|
692
|
+
*
|
|
693
|
+
* The identifier is whatever the caller put in the request, so anything
|
|
694
|
+
* that is not a non-empty string resolves to no order rather than raising.
|
|
695
|
+
*
|
|
696
|
+
* @param array<int, string> $with
|
|
697
|
+
*/
|
|
698
|
+
protected function findOrderById(mixed $id, array $with = []): ?Order
|
|
637
699
|
{
|
|
638
|
-
|
|
700
|
+
if (!is_string($id) || $id === '') {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** @var Order|null $order */
|
|
705
|
+
$order = $this->scopedToCompany(Order::query())
|
|
706
|
+
->where(function (Builder $query) use ($id) {
|
|
707
|
+
$query->where('uuid', $id)->orWhere('public_id', $id);
|
|
708
|
+
})
|
|
709
|
+
->with($with)
|
|
710
|
+
->first();
|
|
711
|
+
|
|
712
|
+
return $order;
|
|
639
713
|
}
|
|
640
714
|
|
|
641
715
|
protected function findOrderRouteForEdit(string $uuid): ?Order
|
|
642
716
|
{
|
|
643
|
-
|
|
717
|
+
/** @var Order|null $order */
|
|
718
|
+
$order = $this->scopedToCompany(Order::where('uuid', $uuid))->with(['payload'])->first();
|
|
719
|
+
|
|
720
|
+
return $order;
|
|
644
721
|
}
|
|
645
722
|
|
|
646
723
|
protected function orderResponse(Order $order): array
|
|
@@ -650,7 +727,7 @@ class OrderController extends FleetOpsController
|
|
|
650
727
|
|
|
651
728
|
protected function assignDriverToOrders($orderUuids, Driver $driver): void
|
|
652
729
|
{
|
|
653
|
-
Order::whereIn('uuid', $orderUuids)->update([
|
|
730
|
+
$this->scopedToCompany(Order::whereIn('uuid', $orderUuids))->update([
|
|
654
731
|
'driver_assigned_uuid' => $driver->uuid,
|
|
655
732
|
'updated_at' => now(),
|
|
656
733
|
]);
|
|
@@ -739,17 +816,28 @@ class OrderController extends FleetOpsController
|
|
|
739
816
|
|
|
740
817
|
protected function findOrderForStart(?string $uuid): ?Order
|
|
741
818
|
{
|
|
742
|
-
|
|
819
|
+
/** @var Order|null $order */
|
|
820
|
+
$order = $this->scopedToCompany(Order::where('uuid', $uuid)->withoutGlobalScopes())->first();
|
|
821
|
+
|
|
822
|
+
return $order;
|
|
743
823
|
}
|
|
744
824
|
|
|
745
825
|
protected function findDriverForStart(?string $uuid): ?Driver
|
|
746
826
|
{
|
|
747
|
-
|
|
827
|
+
/** @var Driver|null $driver */
|
|
828
|
+
$driver = $this->scopedToCompany(Driver::where('uuid', $uuid)->withoutGlobalScopes())->first();
|
|
829
|
+
|
|
830
|
+
return $driver;
|
|
748
831
|
}
|
|
749
832
|
|
|
750
833
|
protected function findPayloadForStart(?string $uuid): ?Payload
|
|
751
834
|
{
|
|
752
|
-
|
|
835
|
+
/** @var Payload|null $payload */
|
|
836
|
+
$payload = $this->scopedToCompany(Payload::where('uuid', $uuid)->withoutGlobalScopes())
|
|
837
|
+
->with(['waypoints', 'waypointMarkers', 'entities'])
|
|
838
|
+
->first();
|
|
839
|
+
|
|
840
|
+
return $payload;
|
|
753
841
|
}
|
|
754
842
|
|
|
755
843
|
protected function dispatchDomainEvent(object $event): object
|
|
@@ -771,7 +859,7 @@ class OrderController extends FleetOpsController
|
|
|
771
859
|
*/
|
|
772
860
|
public function updateActivity(string $id, Request $request)
|
|
773
861
|
{
|
|
774
|
-
$order =
|
|
862
|
+
$order = $this->findOrderById($id, [
|
|
775
863
|
'driverAssigned',
|
|
776
864
|
'payload.entities',
|
|
777
865
|
'payload.pickup',
|
|
@@ -882,9 +970,8 @@ class OrderController extends FleetOpsController
|
|
|
882
970
|
*/
|
|
883
971
|
public function nextActivity(string $id, Request $request)
|
|
884
972
|
{
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
} catch (ModelNotFoundException $e) {
|
|
973
|
+
$order = $this->findOrderById($id);
|
|
974
|
+
if (!$order) {
|
|
888
975
|
return response()->error('No order found.');
|
|
889
976
|
}
|
|
890
977
|
|
|
@@ -935,7 +1022,7 @@ class OrderController extends FleetOpsController
|
|
|
935
1022
|
*/
|
|
936
1023
|
public function setDestination(string $id, string $placeId)
|
|
937
1024
|
{
|
|
938
|
-
$order =
|
|
1025
|
+
$order = $this->findOrderById($id, [
|
|
939
1026
|
'payload.pickup',
|
|
940
1027
|
'payload.dropoff',
|
|
941
1028
|
'payload.return',
|
|
@@ -1013,7 +1100,7 @@ class OrderController extends FleetOpsController
|
|
|
1013
1100
|
return response()->error($errorMessage, 422);
|
|
1014
1101
|
}
|
|
1015
1102
|
|
|
1016
|
-
$order =
|
|
1103
|
+
$order = $this->findOrderById($id, ['payload.pickup', 'payload.dropoff', 'payload.return', 'payload.waypoints', 'payload.waypointMarkers.place']);
|
|
1017
1104
|
if (!$order) {
|
|
1018
1105
|
return response()->error('No order found.');
|
|
1019
1106
|
}
|
|
@@ -1214,7 +1301,14 @@ class OrderController extends FleetOpsController
|
|
|
1214
1301
|
}
|
|
1215
1302
|
|
|
1216
1303
|
if (is_string($proof)) {
|
|
1217
|
-
|
|
1304
|
+
/** @var Proof|null $resolved */
|
|
1305
|
+
$resolved = $this->scopedToCompany(Proof::query())
|
|
1306
|
+
->where(function (Builder $query) use ($proof) {
|
|
1307
|
+
$query->where('public_id', $proof)->orWhere('uuid', $proof);
|
|
1308
|
+
})
|
|
1309
|
+
->first();
|
|
1310
|
+
|
|
1311
|
+
return $resolved;
|
|
1218
1312
|
}
|
|
1219
1313
|
|
|
1220
1314
|
return null;
|
|
@@ -1419,7 +1513,12 @@ class OrderController extends FleetOpsController
|
|
|
1419
1513
|
|
|
1420
1514
|
protected function findOrderForDriverPing(string $id): Order
|
|
1421
1515
|
{
|
|
1422
|
-
|
|
1516
|
+
$order = $this->findOrderById($id, ['driverAssigned']);
|
|
1517
|
+
if (!$order) {
|
|
1518
|
+
throw new ModelNotFoundException();
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
return $order;
|
|
1423
1522
|
}
|
|
1424
1523
|
|
|
1425
1524
|
protected function sendDriverPing(Driver $driver, Order $order): void
|
|
@@ -1692,7 +1791,10 @@ class OrderController extends FleetOpsController
|
|
|
1692
1791
|
|
|
1693
1792
|
protected function findOrderForProofs(string $id): ?Order
|
|
1694
1793
|
{
|
|
1695
|
-
|
|
1794
|
+
/** @var Order|null $order */
|
|
1795
|
+
$order = $this->scopedToCompany(Order::where('uuid', $id))->first();
|
|
1796
|
+
|
|
1797
|
+
return $order;
|
|
1696
1798
|
}
|
|
1697
1799
|
|
|
1698
1800
|
protected function findWaypointProofSubject(Order $order, string $subjectId): ?Waypoint
|
|
@@ -1708,7 +1810,10 @@ class OrderController extends FleetOpsController
|
|
|
1708
1810
|
|
|
1709
1811
|
protected function findEntityProofSubject(string $subjectId): ?Entity
|
|
1710
1812
|
{
|
|
1711
|
-
|
|
1813
|
+
/** @var Entity|null $entity */
|
|
1814
|
+
$entity = $this->scopedToCompany(Entity::where('uuid', $subjectId)->withoutGlobalScopes())->first();
|
|
1815
|
+
|
|
1816
|
+
return $entity;
|
|
1712
1817
|
}
|
|
1713
1818
|
|
|
1714
1819
|
protected function proofsForSubject(Order $order, Order|Waypoint|Entity $subject)
|
|
@@ -1772,14 +1877,26 @@ class OrderController extends FleetOpsController
|
|
|
1772
1877
|
return new OrderResource($order);
|
|
1773
1878
|
}
|
|
1774
1879
|
|
|
1880
|
+
/**
|
|
1881
|
+
* Find the order a tracking number belongs to, for the public Track Order page.
|
|
1882
|
+
*
|
|
1883
|
+
* Deliberately NOT company-scoped, unlike the lifecycle lookups above. The
|
|
1884
|
+
* `fleet-ops/lookup` route is public: the tracking page is used by recipients who
|
|
1885
|
+
* have no session, and the tracking number itself is what grants access. Scoping
|
|
1886
|
+
* it to the session company made every lookup from that page fail, because there
|
|
1887
|
+
* is no company to scope to.
|
|
1888
|
+
*/
|
|
1775
1889
|
protected function findOrderByTrackingNumber(string $trackingNumber): ?Order
|
|
1776
1890
|
{
|
|
1777
|
-
|
|
1891
|
+
/** @var Order|null $order */
|
|
1892
|
+
$order = Order::whereHas(
|
|
1778
1893
|
'trackingNumber',
|
|
1779
1894
|
function ($query) use ($trackingNumber) {
|
|
1780
1895
|
$query->where('tracking_number', $trackingNumber);
|
|
1781
1896
|
}
|
|
1782
1897
|
)->first();
|
|
1898
|
+
|
|
1899
|
+
return $order;
|
|
1783
1900
|
}
|
|
1784
1901
|
|
|
1785
1902
|
/**
|
|
@@ -1824,13 +1941,25 @@ class OrderController extends FleetOpsController
|
|
|
1824
1941
|
|
|
1825
1942
|
protected function findOrderForSchedule(?string $id): ?Order
|
|
1826
1943
|
{
|
|
1827
|
-
return
|
|
1944
|
+
return $this->findOrderById($id);
|
|
1828
1945
|
}
|
|
1829
1946
|
|
|
1947
|
+
/**
|
|
1948
|
+
* Resolve a driver by uuid or public_id, constrained to the caller's company.
|
|
1949
|
+
*
|
|
1950
|
+
* The identifier match is grouped so the company constraint applies to both
|
|
1951
|
+
* arms — ungrouped it would read as `uuid = ? OR (public_id = ? AND
|
|
1952
|
+
* company_uuid = ?)` and still resolve another organization's drivers.
|
|
1953
|
+
*/
|
|
1830
1954
|
protected function findDriverForSchedule(string $id): ?Driver
|
|
1831
1955
|
{
|
|
1832
|
-
|
|
1833
|
-
|
|
1956
|
+
/** @var Driver|null $driver */
|
|
1957
|
+
$driver = $this->scopedToCompany(Driver::query())
|
|
1958
|
+
->where(function (Builder $query) use ($id) {
|
|
1959
|
+
$query->where('uuid', $id)->orWhere('public_id', $id);
|
|
1960
|
+
})
|
|
1834
1961
|
->first();
|
|
1962
|
+
|
|
1963
|
+
return $driver;
|
|
1835
1964
|
}
|
|
1836
1965
|
}
|
|
@@ -41,9 +41,13 @@ class CreateDriverRequest extends FleetbaseRequest
|
|
|
41
41
|
* value later. Both are still validated and still unique when they
|
|
42
42
|
* are supplied; a driver created without them simply cannot sign in
|
|
43
43
|
* to Navigator until credentials are added.
|
|
44
|
+
*
|
|
45
|
+
* On create, availability is decided by ProfileAccountManager: a team
|
|
46
|
+
* member of the company with the email or phone is linked to the new
|
|
47
|
+
* driver instead of being rejected as a duplicate.
|
|
44
48
|
*/
|
|
45
|
-
'email' => ['nullable', Rule::when($this->filled('email'), ['email']), $this->uniqueAmongUsers()],
|
|
46
|
-
'phone' => ['nullable', 'string', $this->uniqueAmongUsers()],
|
|
49
|
+
'email' => ['nullable', Rule::when($this->filled('email'), ['email']), Rule::when(!$isCreating, [$this->uniqueAmongUsers()])],
|
|
50
|
+
'phone' => ['nullable', 'string', Rule::when(!$isCreating, [$this->uniqueAmongUsers()])],
|
|
47
51
|
'password' => 'nullable|string',
|
|
48
52
|
'timezone' => 'nullable|string|max:64',
|
|
49
53
|
|
|
@@ -23,22 +23,17 @@ class CreateDriverRequest extends CreateDriverApiRequest
|
|
|
23
23
|
*/
|
|
24
24
|
public function rules(): array
|
|
25
25
|
{
|
|
26
|
-
$isCreating
|
|
27
|
-
$isCreatingWithUser = $this->filled('driver.user_uuid');
|
|
28
|
-
$shouldValidateUserAttributes = $isCreating && !$isCreatingWithUser;
|
|
26
|
+
$isCreating = $this->isMethod('POST');
|
|
29
27
|
|
|
30
28
|
return [
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
],
|
|
38
|
-
'phone' => [
|
|
39
|
-
Rule::requiredIf($shouldValidateUserAttributes),
|
|
40
|
-
Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]),
|
|
41
|
-
],
|
|
29
|
+
// The driver profile manages its login account: name, email and phone
|
|
30
|
+
// are proxied to it. Email/phone availability is checked against the
|
|
31
|
+
// account the profile resolves to (see ProfileAccountManager), because a
|
|
32
|
+
// team member of the organization with the same email or phone is linked
|
|
33
|
+
// rather than rejected as a duplicate.
|
|
34
|
+
'name' => [Rule::requiredIf($isCreating), 'nullable', 'string', 'max:255'],
|
|
35
|
+
'email' => ['nullable', Rule::when($this->filled('email'), ['email'])],
|
|
36
|
+
'phone' => ['nullable', 'string'],
|
|
42
37
|
|
|
43
38
|
// Optional fields
|
|
44
39
|
'password' => 'nullable|string|min:8',
|
|
@@ -84,11 +79,7 @@ class CreateDriverRequest extends CreateDriverApiRequest
|
|
|
84
79
|
{
|
|
85
80
|
return [
|
|
86
81
|
'name.required' => 'Driver name is required.',
|
|
87
|
-
'email.required' => 'Email address is required.',
|
|
88
82
|
'email.email' => 'Please provide a valid email address.',
|
|
89
|
-
'email.unique' => 'This email address is already registered.',
|
|
90
|
-
'phone.required' => 'Phone number is required.',
|
|
91
|
-
'phone.unique' => 'This phone number is already registered.',
|
|
92
83
|
'password.min' => 'Password must be at least 8 characters.',
|
|
93
84
|
];
|
|
94
85
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Support\ProfileAccountManager;
|
|
5
6
|
use Fleetbase\FleetOps\Support\Utils;
|
|
6
7
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
7
8
|
use Fleetbase\Http\Resources\User;
|
|
@@ -40,6 +41,8 @@ class Contact extends FleetbaseResource
|
|
|
40
41
|
'places' => $this->whenLoaded('places', fn () => Place::collection($this->places)->without('owner')),
|
|
41
42
|
'user' => $this->when(Http::isInternalRequest(), fn () => new User($this->user), fn () => $this->user ? $this->user->public_id : null),
|
|
42
43
|
'address' => $this->when(Http::isInternalRequest(), data_get($this, 'place.address')),
|
|
44
|
+
'is_staff_linked' => $this->when(Http::isInternalRequest(), fn () => ProfileAccountManager::isStaffAccount($this->user)),
|
|
45
|
+
'login_status' => $this->when(Http::isInternalRequest(), fn () => data_get($this, 'user.status')),
|
|
43
46
|
'address_street' => $this->when(Http::isInternalRequest(), data_get($this, 'place.street1')),
|
|
44
47
|
'type' => $this->type ?? null,
|
|
45
48
|
'customer_type' => $this->when(isset($this->customer_type), Utils::toEmberResourceType($this->customer_type)),
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Http\Resources\v1;
|
|
4
4
|
|
|
5
|
+
use Fleetbase\FleetOps\Support\ProfileAccountManager;
|
|
5
6
|
use Fleetbase\FleetOps\Support\Utils;
|
|
6
7
|
use Fleetbase\Http\Resources\FleetbaseResource;
|
|
7
8
|
use Fleetbase\Http\Resources\FleetbaseResourceCollection;
|
|
@@ -32,6 +33,8 @@ class Driver extends FleetbaseResource
|
|
|
32
33
|
'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
|
|
33
34
|
'user' => $this->when(Http::isPublicRequest(), fn () => $this->user ? $this->user->public_id : null, new User($this->user)),
|
|
34
35
|
'internal_id' => $this->internal_id,
|
|
36
|
+
'is_staff_linked' => $this->when(Http::isInternalRequest(), fn () => ProfileAccountManager::isStaffAccount($this->user)),
|
|
37
|
+
'login_status' => $this->when(Http::isInternalRequest(), fn () => data_get($this, 'user.status')),
|
|
35
38
|
'company' => $this->when(Http::isPublicRequest(), fn () => $this->company ? $this->company->public_id : null),
|
|
36
39
|
'company_name' => $this->when(Http::isPublicRequest(), fn () => $this->company ? $this->company->name : null),
|
|
37
40
|
'name' => $this->name,
|
|
@@ -109,6 +109,7 @@ class PollTelematicTelemetry implements ShouldQueue, ShouldBeUnique
|
|
|
109
109
|
try {
|
|
110
110
|
$started = microtime(true);
|
|
111
111
|
DB::table('telematic_sync_runs')->insert(['uuid' => $run, 'telematic_uuid' => $telematic->uuid, 'status' => 'fetching', 'created_at' => now(), 'updated_at' => now()]);
|
|
112
|
+
$this->adoptPendingRequest($run);
|
|
112
113
|
$this->manualProgress([
|
|
113
114
|
'last_sync_run_uuid' => $run, 'last_sync_run_job_id' => $this->manualJobId,
|
|
114
115
|
'last_sync_result' => 'running', 'last_sync_phase' => 'fetching_inventory',
|
|
@@ -186,6 +187,41 @@ class PollTelematicTelemetry implements ShouldQueue, ShouldBeUnique
|
|
|
186
187
|
return $remaining;
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Bind a manual request that has no active run to this scheduled sweep.
|
|
192
|
+
*
|
|
193
|
+
* Every poll job for a connection shares one unique lease, so while a scheduled sweep
|
|
194
|
+
* runs, the request's own job is either not queued (the request was recorded while
|
|
195
|
+
* the lease was held) or no longer exists (lost before or after an interrupted run).
|
|
196
|
+
*/
|
|
197
|
+
private function adoptPendingRequest(string $run): void
|
|
198
|
+
{
|
|
199
|
+
if ($this->manualJobId) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
DB::transaction(function () use ($run) {
|
|
203
|
+
$connection = Telematic::withoutGlobalScopes()->where('uuid', $this->telematicUuid)->lockForUpdate()->first();
|
|
204
|
+
$meta = $connection?->meta ?? [];
|
|
205
|
+
$request = data_get($meta, 'last_sync_job_id');
|
|
206
|
+
if (!$request || !in_array(data_get($meta, 'last_sync_result'), Inbox::PENDING_REQUEST_RESULTS, true)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
$bound = data_get($meta, 'last_sync_run_uuid');
|
|
210
|
+
if ($bound && DB::table('telematic_sync_runs')->where('uuid', $bound)->whereIn('status', ['fetching', 'ingesting'])->exists()) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
$connection->status = 'synchronizing';
|
|
214
|
+
$connection->meta = array_merge($meta, [
|
|
215
|
+
'last_sync_run_uuid' => $run, 'last_sync_run_job_id' => $request,
|
|
216
|
+
'last_sync_result' => 'running', 'last_sync_phase' => 'fetching_inventory',
|
|
217
|
+
'last_sync_fetched_total' => 0, 'last_sync_page_count' => 0,
|
|
218
|
+
'last_sync_linked_total' => 0, 'last_sync_failed_total' => 0, 'last_sync_error' => null,
|
|
219
|
+
'last_sync_progress_at' => now()->toDateTimeString(),
|
|
220
|
+
]);
|
|
221
|
+
$connection->save();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
189
225
|
private function manualProgress(array $attributes): void
|
|
190
226
|
{
|
|
191
227
|
if (!$this->manualJobId) {
|
|
@@ -58,8 +58,8 @@ class SyncTelematicDevicesJob implements ShouldQueue
|
|
|
58
58
|
try {
|
|
59
59
|
$service->queueTelemetrySync($this->telematic, $this->options, $this->jobId);
|
|
60
60
|
} catch (\Illuminate\Validation\ValidationException) {
|
|
61
|
-
//
|
|
62
|
-
// as errored and detach
|
|
61
|
+
// Polling was paused after this job was queued. Failing here would mark the
|
|
62
|
+
// connection as errored and detach any active request's progress tracking.
|
|
63
63
|
Log::info('Skipped legacy telematics sync redirect.', ['telematic_uuid' => $this->telematic->uuid]);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Mail;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
6
|
+
use Fleetbase\Models\User;
|
|
7
|
+
use Illuminate\Bus\Queueable;
|
|
8
|
+
use Illuminate\Mail\Mailable;
|
|
9
|
+
use Illuminate\Mail\Mailables\Content;
|
|
10
|
+
use Illuminate\Mail\Mailables\Envelope;
|
|
11
|
+
use Illuminate\Queue\SerializesModels;
|
|
12
|
+
|
|
13
|
+
class DriverCredentialsMail extends Mailable
|
|
14
|
+
{
|
|
15
|
+
use Queueable;
|
|
16
|
+
use SerializesModels;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a new message instance.
|
|
20
|
+
*/
|
|
21
|
+
public function __construct(private string $plaintextPassword, private Driver $driver, private User $user)
|
|
22
|
+
{
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the message envelope.
|
|
27
|
+
*/
|
|
28
|
+
public function envelope(): Envelope
|
|
29
|
+
{
|
|
30
|
+
$this->driver->loadMissing('company');
|
|
31
|
+
|
|
32
|
+
return new Envelope(
|
|
33
|
+
subject: 'Your ' . ($this->driver->company?->name ?? config('app.name')) . ' driver sign-in details',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the message content definition.
|
|
39
|
+
*/
|
|
40
|
+
public function content(): Content
|
|
41
|
+
{
|
|
42
|
+
$this->driver->loadMissing('company');
|
|
43
|
+
|
|
44
|
+
return new Content(
|
|
45
|
+
markdown: 'fleetops::mail.driver-credentials',
|
|
46
|
+
with: [
|
|
47
|
+
'driver' => $this->driver,
|
|
48
|
+
'user' => $this->user,
|
|
49
|
+
'companyName' => $this->driver->company?->name ?? config('app.name'),
|
|
50
|
+
'identity' => $this->user->email ?? $this->user->phone,
|
|
51
|
+
'plaintextPassword' => $this->plaintextPassword,
|
|
52
|
+
]
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|