@fleetbase/fleetops-engine 0.6.43 → 0.6.45
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/addon/components/entity/form.hbs +24 -0
- package/addon/components/entity/form.js +30 -0
- package/addon/components/leaflet-draw-control.js +1 -1
- package/addon/components/map/drawer/geofence-event-listing.hbs +123 -0
- package/addon/components/map/drawer/geofence-event-listing.js +103 -0
- package/addon/components/map/drawer.js +1 -0
- package/addon/components/map/leaflet-live-map.js +83 -0
- package/addon/components/modals/orchestrator-import.js +2 -78
- package/addon/components/order/form/service-rate.hbs +1 -1
- package/addon/components/service-area/form.hbs +59 -0
- package/addon/components/service-rate/details.hbs +6 -6
- package/addon/components/service-rate/form.hbs +28 -15
- package/addon/components/zone/form.hbs +60 -1
- package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
- package/addon/controllers/operations/orders/index/details.js +27 -2
- package/addon/controllers/operations/service-rates/index/edit.js +1 -0
- package/addon/controllers/operations/service-rates/index/new.js +1 -0
- package/addon/helpers/f-to-int.js +6 -0
- package/addon/routes/application.js +3 -3
- package/addon/routes/operations/orders/index/details/virtual.js +1 -0
- package/addon/routes/operations/orders/index/details.js +19 -3
- package/addon/services/equipment-actions.js +4 -6
- package/addon/services/geofence-event-bus.js +223 -0
- package/addon/services/leaflet-map-manager.js +4 -0
- package/addon/services/maintenance-actions.js +4 -6
- package/addon/services/part-actions.js +4 -6
- package/addon/services/service-rate-actions.js +8 -3
- package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
- package/addon/utils/fleet-ops-options.js +33 -0
- package/addon/utils/serialize-payload.js +1 -1
- package/app/components/map/drawer/geofence-event-listing.js +1 -0
- package/app/controllers/operations/orders/index/details/virtual.js +1 -0
- package/app/helpers/f-to-int.js +1 -0
- package/app/services/geofence-event-bus.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +2 -2
- package/server/migrations/2026_04_05_000001_add_geofence_config_to_zones_table.php +48 -0
- package/server/migrations/2026_04_05_000002_add_geofence_config_to_service_areas_table.php +38 -0
- package/server/migrations/2026_04_05_000003_create_driver_geofence_states_table.php +62 -0
- package/server/migrations/2026_04_05_000004_create_geofence_events_log_table.php +66 -0
- package/server/migrations/2026_04_22_000001_add_vehicle_support_to_geofence_events_log.php +31 -0
- package/server/migrations/2026_04_22_000002_create_vehicle_geofence_states_table.php +39 -0
- package/server/src/Console/Commands/SimulateGeofenceEvents.php +278 -0
- package/server/src/Events/GeofenceDwelled.php +208 -0
- package/server/src/Events/GeofenceEntered.php +207 -0
- package/server/src/Events/GeofenceExited.php +213 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +100 -0
- package/server/src/Http/Controllers/Api/v1/GeofenceController.php +263 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +13 -8
- package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -2
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +4 -0
- package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +88 -0
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -2
- package/server/src/Http/Controllers/Internal/v1/GeofenceController.php +7 -0
- package/server/src/Http/Requests/CreateServiceAreaRequest.php +8 -4
- package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
- package/server/src/Http/Requests/CreateZoneRequest.php +8 -4
- package/server/src/Http/Resources/v1/Order.php +0 -1
- package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
- package/server/src/Jobs/CheckGeofenceDwell.php +139 -0
- package/server/src/Listeners/HandleGeofenceDwelled.php +56 -0
- package/server/src/Listeners/HandleGeofenceEntered.php +187 -0
- package/server/src/Listeners/HandleGeofenceExited.php +56 -0
- package/server/src/Models/Entity.php +7 -0
- package/server/src/Models/GeofenceEventLog.php +155 -0
- package/server/src/Models/Order.php +39 -7
- package/server/src/Models/ServiceArea.php +21 -2
- package/server/src/Models/ServiceQuote.php +3 -1
- package/server/src/Models/ServiceRate.php +122 -30
- package/server/src/Models/Zone.php +20 -2
- package/server/src/Notifications/DriverArrivedAtGeofence.php +86 -0
- package/server/src/Observers/PurchaseRateObserver.php +3 -5
- package/server/src/Providers/EventServiceProvider.php +19 -0
- package/server/src/Providers/FleetOpsServiceProvider.php +10 -0
- package/server/src/Rules/ComputableAlgo.php +1 -3
- package/server/src/Support/Algo.php +87 -1
- package/server/src/Support/GeofenceIntersectionService.php +206 -0
- package/server/src/Support/Utils.php +35 -0
- package/server/src/routes.php +16 -0
|
@@ -369,11 +369,37 @@ class ServiceRate extends Model
|
|
|
369
369
|
return $this;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
// Normalize duplicate parcel rows by fee shape and keep the latest submitted value.
|
|
373
|
+
$serviceRateParcelFees = collect($serviceRateParcelFees)
|
|
374
|
+
->filter(fn ($fee) => is_array($fee))
|
|
375
|
+
->map(function ($fee) {
|
|
376
|
+
return collect($fee)->except(['created_at', 'updated_at'])->toArray();
|
|
377
|
+
})
|
|
378
|
+
->keyBy(function ($fee) {
|
|
379
|
+
return implode(':', [
|
|
380
|
+
data_get($fee, 'size'),
|
|
381
|
+
data_get($fee, 'length'),
|
|
382
|
+
data_get($fee, 'width'),
|
|
383
|
+
data_get($fee, 'height'),
|
|
384
|
+
data_get($fee, 'dimensions_unit'),
|
|
385
|
+
data_get($fee, 'weight'),
|
|
386
|
+
data_get($fee, 'weight_unit'),
|
|
387
|
+
]);
|
|
388
|
+
})
|
|
389
|
+
->values()
|
|
390
|
+
->toArray();
|
|
391
|
+
|
|
392
|
+
$submittedUuids = collect($serviceRateParcelFees)
|
|
393
|
+
->pluck('uuid')
|
|
394
|
+
->filter()
|
|
395
|
+
->values()
|
|
396
|
+
->all();
|
|
397
|
+
|
|
372
398
|
$iterate = count($serviceRateParcelFees);
|
|
373
399
|
|
|
374
400
|
for ($i = 0; $i < $iterate; $i++) {
|
|
375
401
|
// if already has uuid then we just update the record and remove from insert array
|
|
376
|
-
if (
|
|
402
|
+
if (!empty($serviceRateParcelFees[$i]['uuid'])) {
|
|
377
403
|
$id = $serviceRateParcelFees[$i]['uuid'];
|
|
378
404
|
$updateableAttributes = collect($serviceRateParcelFees[$i])->except(['uuid', 'created_at', 'updated_at'])->toArray();
|
|
379
405
|
|
|
@@ -391,7 +417,18 @@ class ServiceRate extends Model
|
|
|
391
417
|
}
|
|
392
418
|
|
|
393
419
|
$serviceRateParcelFees = collect($serviceRateParcelFees)->filter()->values()->toArray();
|
|
394
|
-
|
|
420
|
+
|
|
421
|
+
$existingParcelFeesQuery = ServiceRateParcelFee::where('service_rate_uuid', $this->uuid);
|
|
422
|
+
|
|
423
|
+
if (!empty($submittedUuids)) {
|
|
424
|
+
$existingParcelFeesQuery->whereNotIn('uuid', $submittedUuids)->delete();
|
|
425
|
+
} else {
|
|
426
|
+
$existingParcelFeesQuery->delete();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!empty($serviceRateParcelFees)) {
|
|
430
|
+
ServiceRateParcelFee::bulkInsert($serviceRateParcelFees);
|
|
431
|
+
}
|
|
395
432
|
|
|
396
433
|
return $this;
|
|
397
434
|
}
|
|
@@ -688,13 +725,13 @@ class ServiceRate extends Model
|
|
|
688
725
|
}
|
|
689
726
|
|
|
690
727
|
if ($this->isPerMeter()) {
|
|
691
|
-
$perMeterDistance = $this->
|
|
692
|
-
$rateFee = $perMeterDistance * $this->per_meter_flat_rate_fee;
|
|
728
|
+
$perMeterDistance = $this->normalizeDistanceForUnit($totalDistance, $this->per_meter_unit);
|
|
729
|
+
$rateFee = $this->normalizeCalculatedMoney($perMeterDistance * $this->per_meter_flat_rate_fee);
|
|
693
730
|
$subTotal += $rateFee;
|
|
694
731
|
|
|
695
732
|
$lines->push([
|
|
696
733
|
'details' => 'Service Fee',
|
|
697
|
-
'amount' =>
|
|
734
|
+
'amount' => $rateFee,
|
|
698
735
|
'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
|
|
699
736
|
'currency' => $this->currency,
|
|
700
737
|
'code' => 'BASE_FEE',
|
|
@@ -702,20 +739,23 @@ class ServiceRate extends Model
|
|
|
702
739
|
}
|
|
703
740
|
|
|
704
741
|
if ($this->isAlgorithm()) {
|
|
705
|
-
$rateFee = Algo::exec(
|
|
742
|
+
$rateFee = $this->normalizeCalculatedMoney(Algo::exec(
|
|
706
743
|
$this->algorithm,
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
744
|
+
$this->buildAlgorithmVariables(
|
|
745
|
+
$entities,
|
|
746
|
+
$waypoints,
|
|
747
|
+
$totalDistance,
|
|
748
|
+
$totalTime,
|
|
749
|
+
$this->inferEndpointCountFromStops($waypoints)
|
|
750
|
+
),
|
|
711
751
|
true
|
|
712
|
-
);
|
|
752
|
+
));
|
|
713
753
|
|
|
714
|
-
$subTotal +=
|
|
754
|
+
$subTotal += $rateFee;
|
|
715
755
|
|
|
716
756
|
$lines->push([
|
|
717
757
|
'details' => 'Service Fee',
|
|
718
|
-
'amount' =>
|
|
758
|
+
'amount' => $rateFee,
|
|
719
759
|
'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
|
|
720
760
|
'currency' => $this->currency,
|
|
721
761
|
'code' => 'BASE_FEE',
|
|
@@ -758,9 +798,10 @@ class ServiceRate extends Model
|
|
|
758
798
|
}
|
|
759
799
|
|
|
760
800
|
$subTotal += $serviceParcelFee->fee;
|
|
801
|
+
$parcelFeeName = ucwords(str_replace(['_', '-'], ' ', data_get($serviceParcelFee, 'size', 'parcel')));
|
|
761
802
|
|
|
762
803
|
$lines->push([
|
|
763
|
-
'details' => $
|
|
804
|
+
'details' => $parcelFeeName . ' parcel fee',
|
|
764
805
|
'amount' => Utils::numbersOnly($serviceParcelFee->fee),
|
|
765
806
|
'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
|
|
766
807
|
'currency' => $this->currency,
|
|
@@ -777,7 +818,7 @@ class ServiceRate extends Model
|
|
|
777
818
|
if ($this->hasCodFlatFee()) {
|
|
778
819
|
$subTotal += $codFee = $this->cod_flat_fee;
|
|
779
820
|
} elseif ($this->hasCodPercentageFee()) {
|
|
780
|
-
$subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
|
|
821
|
+
$subTotal += $codFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->cod_percent, $baseRate));
|
|
781
822
|
}
|
|
782
823
|
|
|
783
824
|
$lines->push([
|
|
@@ -794,7 +835,7 @@ class ServiceRate extends Model
|
|
|
794
835
|
if ($this->hasPeakHoursFlatFee()) {
|
|
795
836
|
$subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
|
|
796
837
|
} elseif ($this->hasPeakHoursPercentageFee()) {
|
|
797
|
-
$subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
|
|
838
|
+
$subTotal += $peakHoursFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->peak_hours_percent, $baseRate));
|
|
798
839
|
}
|
|
799
840
|
|
|
800
841
|
$lines->push([
|
|
@@ -872,13 +913,13 @@ class ServiceRate extends Model
|
|
|
872
913
|
}
|
|
873
914
|
|
|
874
915
|
if ($this->isPerMeter()) {
|
|
875
|
-
$perMeterDistance = $this->
|
|
876
|
-
$rateFee = $perMeterDistance * $this->per_meter_flat_rate_fee;
|
|
916
|
+
$perMeterDistance = $this->normalizeDistanceForUnit($totalDistance, $this->per_meter_unit);
|
|
917
|
+
$rateFee = $this->normalizeCalculatedMoney($perMeterDistance * $this->per_meter_flat_rate_fee);
|
|
877
918
|
$subTotal += $rateFee;
|
|
878
919
|
|
|
879
920
|
$lines->push([
|
|
880
921
|
'details' => 'Service Fee',
|
|
881
|
-
'amount' =>
|
|
922
|
+
'amount' => $rateFee,
|
|
882
923
|
'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
|
|
883
924
|
'currency' => $this->currency,
|
|
884
925
|
'code' => 'BASE_FEE',
|
|
@@ -886,20 +927,23 @@ class ServiceRate extends Model
|
|
|
886
927
|
}
|
|
887
928
|
|
|
888
929
|
if ($this->isAlgorithm()) {
|
|
889
|
-
$rateFee = Algo::exec(
|
|
930
|
+
$rateFee = $this->normalizeCalculatedMoney(Algo::exec(
|
|
890
931
|
$this->algorithm,
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
932
|
+
$this->buildAlgorithmVariables(
|
|
933
|
+
$payload->entities->all(),
|
|
934
|
+
$waypoints->all(),
|
|
935
|
+
$totalDistance,
|
|
936
|
+
$totalTime,
|
|
937
|
+
(int) ($payload->pickup ? 1 : 0) + (int) ($payload->dropoff ? 1 : 0)
|
|
938
|
+
),
|
|
895
939
|
true
|
|
896
|
-
);
|
|
940
|
+
));
|
|
897
941
|
|
|
898
|
-
$subTotal +=
|
|
942
|
+
$subTotal += $rateFee;
|
|
899
943
|
|
|
900
944
|
$lines->push([
|
|
901
945
|
'details' => 'Service Fee',
|
|
902
|
-
'amount' =>
|
|
946
|
+
'amount' => $rateFee,
|
|
903
947
|
'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
|
|
904
948
|
'currency' => $this->currency,
|
|
905
949
|
'code' => 'BASE_FEE',
|
|
@@ -942,9 +986,10 @@ class ServiceRate extends Model
|
|
|
942
986
|
}
|
|
943
987
|
|
|
944
988
|
$subTotal += $serviceParcelFee->fee;
|
|
989
|
+
$parcelFeeName = ucwords(str_replace(['_', '-'], ' ', data_get($serviceParcelFee, 'size', 'parcel')));
|
|
945
990
|
|
|
946
991
|
$lines->push([
|
|
947
|
-
'details' => $
|
|
992
|
+
'details' => $parcelFeeName . ' parcel fee',
|
|
948
993
|
'amount' => Utils::numbersOnly($serviceParcelFee->fee),
|
|
949
994
|
'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
|
|
950
995
|
'currency' => $this->currency,
|
|
@@ -961,7 +1006,7 @@ class ServiceRate extends Model
|
|
|
961
1006
|
if ($this->hasCodFlatFee()) {
|
|
962
1007
|
$subTotal += $codFee = $this->cod_flat_fee;
|
|
963
1008
|
} elseif ($this->hasCodPercentageFee()) {
|
|
964
|
-
$subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
|
|
1009
|
+
$subTotal += $codFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->cod_percent, $baseRate));
|
|
965
1010
|
}
|
|
966
1011
|
|
|
967
1012
|
$lines->push([
|
|
@@ -978,7 +1023,7 @@ class ServiceRate extends Model
|
|
|
978
1023
|
if ($this->hasPeakHoursFlatFee()) {
|
|
979
1024
|
$subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
|
|
980
1025
|
} elseif ($this->hasPeakHoursPercentageFee()) {
|
|
981
|
-
$subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
|
|
1026
|
+
$subTotal += $peakHoursFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->peak_hours_percent, $baseRate));
|
|
982
1027
|
}
|
|
983
1028
|
|
|
984
1029
|
$lines->push([
|
|
@@ -993,6 +1038,53 @@ class ServiceRate extends Model
|
|
|
993
1038
|
return [$subTotal, $lines];
|
|
994
1039
|
}
|
|
995
1040
|
|
|
1041
|
+
protected function normalizeDistanceForUnit(?int $distanceInMeters = 0, ?string $unit = 'm'): float|int
|
|
1042
|
+
{
|
|
1043
|
+
$distanceInMeters = (float) ($distanceInMeters ?? 0);
|
|
1044
|
+
|
|
1045
|
+
return match ($unit) {
|
|
1046
|
+
'km' => $distanceInMeters / 1000,
|
|
1047
|
+
'ft' => $distanceInMeters / 0.3048,
|
|
1048
|
+
'yd' => $distanceInMeters / 0.9144,
|
|
1049
|
+
'mi' => $distanceInMeters / 1609.344,
|
|
1050
|
+
default => $distanceInMeters,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
protected function normalizeCalculatedMoney($amount = 0): int
|
|
1055
|
+
{
|
|
1056
|
+
return (int) round((float) ($amount ?? 0));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
protected function buildAlgorithmVariables($entities = [], $stops = [], ?int $totalDistance = 0, ?int $totalTime = 0, int $endpointCount = 0): array
|
|
1060
|
+
{
|
|
1061
|
+
$entityCollection = collect($entities)->filter();
|
|
1062
|
+
$stopCount = collect($stops)->filter()->count();
|
|
1063
|
+
$endpointCount = max(0, min($endpointCount, $stopCount));
|
|
1064
|
+
$waypointCount = max($stopCount - $endpointCount, 0);
|
|
1065
|
+
|
|
1066
|
+
return Algo::normalizeVariables([
|
|
1067
|
+
'distance_m' => $totalDistance ?? 0,
|
|
1068
|
+
'time_s' => $totalTime ?? 0,
|
|
1069
|
+
'stops' => $stopCount,
|
|
1070
|
+
'waypoints' => $waypointCount,
|
|
1071
|
+
'parcels' => $entityCollection->where('type', 'parcel')->count(),
|
|
1072
|
+
'entities' => $entityCollection->count(),
|
|
1073
|
+
'base_fee' => Utils::numbersOnly($this->base_fee ?? 0),
|
|
1074
|
+
]);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
protected function inferEndpointCountFromStops($stops = []): int
|
|
1078
|
+
{
|
|
1079
|
+
$stopCount = collect($stops)->filter()->count();
|
|
1080
|
+
|
|
1081
|
+
if ($stopCount <= 1) {
|
|
1082
|
+
return $stopCount;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
return 2;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
996
1088
|
/**
|
|
997
1089
|
* Find the ServiceRateFee based on the total distance.
|
|
998
1090
|
*
|
|
@@ -62,7 +62,21 @@ class Zone extends Model
|
|
|
62
62
|
*
|
|
63
63
|
* @var array
|
|
64
64
|
*/
|
|
65
|
-
protected $fillable = [
|
|
65
|
+
protected $fillable = [
|
|
66
|
+
'_key',
|
|
67
|
+
'company_uuid',
|
|
68
|
+
'service_area_uuid',
|
|
69
|
+
'name',
|
|
70
|
+
'description',
|
|
71
|
+
'border',
|
|
72
|
+
'color',
|
|
73
|
+
'stroke_color',
|
|
74
|
+
'status',
|
|
75
|
+
'trigger_on_entry',
|
|
76
|
+
'trigger_on_exit',
|
|
77
|
+
'dwell_threshold_minutes',
|
|
78
|
+
'speed_limit_kmh',
|
|
79
|
+
];
|
|
66
80
|
|
|
67
81
|
/**
|
|
68
82
|
* The attributes that should be cast to native types.
|
|
@@ -70,7 +84,11 @@ class Zone extends Model
|
|
|
70
84
|
* @var array
|
|
71
85
|
*/
|
|
72
86
|
protected $casts = [
|
|
73
|
-
'border'
|
|
87
|
+
'border' => PolygonCast::class,
|
|
88
|
+
'trigger_on_entry' => 'boolean',
|
|
89
|
+
'trigger_on_exit' => 'boolean',
|
|
90
|
+
'dwell_threshold_minutes' => 'integer',
|
|
91
|
+
'speed_limit_kmh' => 'integer',
|
|
74
92
|
];
|
|
75
93
|
|
|
76
94
|
/**
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Fleetbase\FleetOps\Notifications;
|
|
4
|
+
|
|
5
|
+
use Fleetbase\FleetOps\Models\Order;
|
|
6
|
+
use Illuminate\Bus\Queueable;
|
|
7
|
+
use Illuminate\Notifications\Messages\MailMessage;
|
|
8
|
+
use Illuminate\Notifications\Notification;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DriverArrivedAtGeofence.
|
|
12
|
+
*
|
|
13
|
+
* Sent to the order customer when a driver enters the geofence associated
|
|
14
|
+
* with the order's current destination waypoint.
|
|
15
|
+
*
|
|
16
|
+
* Delivery channels: mail and database.
|
|
17
|
+
*/
|
|
18
|
+
class DriverArrivedAtGeofence extends Notification
|
|
19
|
+
{
|
|
20
|
+
use Queueable;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The order associated with the arrival.
|
|
24
|
+
*/
|
|
25
|
+
protected Order $order;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The geofence that was entered (Zone or ServiceArea).
|
|
29
|
+
*/
|
|
30
|
+
protected $geofence;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a new DriverArrivedAtGeofence notification.
|
|
34
|
+
*
|
|
35
|
+
* @param mixed $geofence Zone or ServiceArea
|
|
36
|
+
*/
|
|
37
|
+
public function __construct(Order $order, $geofence)
|
|
38
|
+
{
|
|
39
|
+
$this->order = $order;
|
|
40
|
+
$this->geofence = $geofence;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the notification's delivery channels.
|
|
45
|
+
*/
|
|
46
|
+
public function via($notifiable): array
|
|
47
|
+
{
|
|
48
|
+
return ['mail', 'database'];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the mail representation of the notification.
|
|
53
|
+
*/
|
|
54
|
+
public function toMail($notifiable): MailMessage
|
|
55
|
+
{
|
|
56
|
+
$geofenceName = $this->geofence->name ?? 'your location';
|
|
57
|
+
$orderId = $this->order->public_id ?? $this->order->uuid;
|
|
58
|
+
|
|
59
|
+
return (new MailMessage())
|
|
60
|
+
->subject("Your driver has arrived — Order #{$orderId}")
|
|
61
|
+
->greeting('Good news!')
|
|
62
|
+
->line("Your driver has arrived at {$geofenceName} for order #{$orderId}.")
|
|
63
|
+
->line('Please be ready to receive your delivery.')
|
|
64
|
+
->action('Track Your Order', url("/tracking/{$this->order->tracking_number}"))
|
|
65
|
+
->line('Thank you for using our service.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get the array representation of the notification (database channel).
|
|
70
|
+
*/
|
|
71
|
+
public function toArray($notifiable): array
|
|
72
|
+
{
|
|
73
|
+
return [
|
|
74
|
+
'event' => 'driver.arrived_at_geofence',
|
|
75
|
+
'order_id' => $this->order->public_id,
|
|
76
|
+
'order_uuid' => $this->order->uuid,
|
|
77
|
+
'geofence_id' => $this->geofence->public_id ?? null,
|
|
78
|
+
'geofence_name' => $this->geofence->name ?? null,
|
|
79
|
+
'message' => sprintf(
|
|
80
|
+
'Your driver has arrived at %s for order #%s.',
|
|
81
|
+
$this->geofence->name ?? 'your location',
|
|
82
|
+
$this->order->public_id
|
|
83
|
+
),
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
namespace Fleetbase\FleetOps\Observers;
|
|
4
4
|
|
|
5
|
-
use Fleetbase\FleetOps\Models\Order;
|
|
6
5
|
use Fleetbase\FleetOps\Models\PurchaseRate;
|
|
7
6
|
use Fleetbase\FleetOps\Support\Utils;
|
|
8
7
|
use Fleetbase\Models\Company;
|
|
@@ -25,7 +24,8 @@ class PurchaseRateObserver
|
|
|
25
24
|
$company = Company::where('uuid', session('company', $purchaseRate->company_uuid))->first();
|
|
26
25
|
|
|
27
26
|
// get currency to use
|
|
28
|
-
$currency = data_get($purchaseRate, 'serviceQuote.currency'
|
|
27
|
+
$currency = data_get($purchaseRate, 'serviceQuote.currency')
|
|
28
|
+
?: Utils::getCompanyTransactionCurrency($company ?? $purchaseRate->company_uuid);
|
|
29
29
|
|
|
30
30
|
// create transaction and transaction items
|
|
31
31
|
$transaction = Transaction::create([
|
|
@@ -41,9 +41,6 @@ class PurchaseRateObserver
|
|
|
41
41
|
'status' => 'success',
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
-
// Update order with transaction id
|
|
45
|
-
Order::where('payload_uuid', $purchaseRate->payload_uuid)->update(['transaction_uuid' => $transaction->uuid]);
|
|
46
|
-
|
|
47
44
|
if (isset($purchaseRate->serviceQuote)) {
|
|
48
45
|
$purchaseRate->serviceQuote->items->each(function ($serviceQuoteItem) use ($transaction, $currency) {
|
|
49
46
|
TransactionItem::create([
|
|
@@ -57,5 +54,6 @@ class PurchaseRateObserver
|
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
$purchaseRate->transaction_uuid = $transaction->uuid;
|
|
57
|
+
$purchaseRate->status = $purchaseRate->status ?: Transaction::STATUS_SUCCESS;
|
|
60
58
|
}
|
|
61
59
|
}
|
|
@@ -23,6 +23,25 @@ class EventServiceProvider extends ServiceProvider
|
|
|
23
23
|
\Fleetbase\FleetOps\Events\OrderFailed::class => [\Fleetbase\Listeners\SendResourceLifecycleWebhook::class, \Fleetbase\FleetOps\Listeners\NotifyOrderEvent::class],
|
|
24
24
|
\Fleetbase\FleetOps\Events\OrderReady::class => [\Fleetbase\FleetOps\Listeners\HandleOrderReady::class],
|
|
25
25
|
|
|
26
|
+
/*
|
|
27
|
+
* Geofence Events
|
|
28
|
+
*
|
|
29
|
+
* Each event is handled by a domain listener (business logic, event log)
|
|
30
|
+
* and the generic SendResourceLifecycleWebhook listener (webhook delivery).
|
|
31
|
+
*/
|
|
32
|
+
\Fleetbase\FleetOps\Events\GeofenceEntered::class => [
|
|
33
|
+
\Fleetbase\FleetOps\Listeners\HandleGeofenceEntered::class,
|
|
34
|
+
\Fleetbase\Listeners\SendResourceLifecycleWebhook::class,
|
|
35
|
+
],
|
|
36
|
+
\Fleetbase\FleetOps\Events\GeofenceExited::class => [
|
|
37
|
+
\Fleetbase\FleetOps\Listeners\HandleGeofenceExited::class,
|
|
38
|
+
\Fleetbase\Listeners\SendResourceLifecycleWebhook::class,
|
|
39
|
+
],
|
|
40
|
+
\Fleetbase\FleetOps\Events\GeofenceDwelled::class => [
|
|
41
|
+
\Fleetbase\FleetOps\Listeners\HandleGeofenceDwelled::class,
|
|
42
|
+
\Fleetbase\Listeners\SendResourceLifecycleWebhook::class,
|
|
43
|
+
],
|
|
44
|
+
|
|
26
45
|
/*
|
|
27
46
|
* Core Events
|
|
28
47
|
*/
|
|
@@ -61,6 +61,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
61
61
|
\Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class,
|
|
62
62
|
\Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class,
|
|
63
63
|
\Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class,
|
|
64
|
+
\Fleetbase\FleetOps\Console\Commands\SimulateGeofenceEvents::class,
|
|
64
65
|
\Fleetbase\FleetOps\Console\Commands\TestEmail::class,
|
|
65
66
|
\Fleetbase\FleetOps\Console\Commands\ProcessMaintenanceTriggers::class,
|
|
66
67
|
\Fleetbase\FleetOps\Console\Commands\SendMaintenanceReminders::class,
|
|
@@ -84,6 +85,14 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
84
85
|
$this->app->register(CoreServiceProvider::class);
|
|
85
86
|
$this->app->register(ReportSchemaServiceProvider::class);
|
|
86
87
|
|
|
88
|
+
// Register the GeofenceIntersectionService as a singleton so that
|
|
89
|
+
// the same instance is reused across the request lifecycle, avoiding
|
|
90
|
+
// repeated instantiation on high-frequency location update calls.
|
|
91
|
+
$this->app->singleton(
|
|
92
|
+
\Fleetbase\FleetOps\Support\GeofenceIntersectionService::class,
|
|
93
|
+
fn () => new \Fleetbase\FleetOps\Support\GeofenceIntersectionService()
|
|
94
|
+
);
|
|
95
|
+
|
|
87
96
|
// Register the OrchestrationEngineRegistry as a singleton so that engines
|
|
88
97
|
// registered from any service provider share the same instance.
|
|
89
98
|
$this->app->singleton(
|
|
@@ -155,6 +164,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider
|
|
|
155
164
|
\Fleetbase\FleetOps\Notifications\OrderPing::class,
|
|
156
165
|
\Fleetbase\FleetOps\Notifications\OrderFailed::class,
|
|
157
166
|
\Fleetbase\FleetOps\Notifications\OrderCompleted::class,
|
|
167
|
+
\Fleetbase\FleetOps\Notifications\DriverArrivedAtGeofence::class,
|
|
158
168
|
]);
|
|
159
169
|
|
|
160
170
|
// Register Notifiables
|
|
@@ -16,9 +16,7 @@ class ComputableAlgo implements Rule
|
|
|
16
16
|
*/
|
|
17
17
|
public function passes($attribute, $value)
|
|
18
18
|
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return Algo::exec($value, $distanceAndTime) > 0;
|
|
19
|
+
return Algo::isComputable($value);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
/**
|
|
@@ -6,6 +6,8 @@ use Webit\Util\EvalMath\EvalMath;
|
|
|
6
6
|
|
|
7
7
|
class Algo
|
|
8
8
|
{
|
|
9
|
+
protected const FUNCTION_PATTERN = '/\b(max|min|ceil|floor|round)\(([^()]*)\)/';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Execute an algorithm strig.
|
|
11
13
|
*
|
|
@@ -16,11 +18,18 @@ class Algo
|
|
|
16
18
|
$m = new EvalMath();
|
|
17
19
|
$m->suppress_errors = true;
|
|
18
20
|
|
|
21
|
+
$variables = static::normalizeVariables($variables);
|
|
22
|
+
|
|
19
23
|
foreach ($variables as $key => $value) {
|
|
20
24
|
$algorithm = str_replace('{' . $key . '}', $value, $algorithm);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
$
|
|
27
|
+
$algorithm = static::evaluateFunctions($algorithm);
|
|
28
|
+
$result = $m->evaluate($algorithm);
|
|
29
|
+
|
|
30
|
+
if ($result === false || $result === null || !is_numeric($result)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
24
33
|
|
|
25
34
|
if ($round) {
|
|
26
35
|
return round($result, 2); // precision 2 cuz most likely dealing with $
|
|
@@ -28,4 +37,81 @@ class Algo
|
|
|
28
37
|
|
|
29
38
|
return $result;
|
|
30
39
|
}
|
|
40
|
+
|
|
41
|
+
public static function isComputable($algorithm): bool
|
|
42
|
+
{
|
|
43
|
+
$result = static::exec($algorithm, static::validationFixture(), true);
|
|
44
|
+
|
|
45
|
+
return $result !== null && is_numeric($result);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static function normalizeVariables($variables = []): array
|
|
49
|
+
{
|
|
50
|
+
$variables = collect($variables)->all();
|
|
51
|
+
|
|
52
|
+
$distanceM = (float) ($variables['distance_m'] ?? $variables['distance'] ?? 0);
|
|
53
|
+
$timeS = (float) ($variables['time_s'] ?? $variables['time'] ?? 0);
|
|
54
|
+
|
|
55
|
+
$normalized = [
|
|
56
|
+
'distance_m' => $distanceM,
|
|
57
|
+
'distance' => $distanceM,
|
|
58
|
+
'distance_km'=> $distanceM / 1000,
|
|
59
|
+
'distance_mi'=> $distanceM / 1609.344,
|
|
60
|
+
'time_s' => $timeS,
|
|
61
|
+
'time' => $timeS,
|
|
62
|
+
'time_min' => $timeS / 60,
|
|
63
|
+
'stops' => (int) ($variables['stops'] ?? 0),
|
|
64
|
+
'waypoints' => (int) ($variables['waypoints'] ?? 0),
|
|
65
|
+
'parcels' => (int) ($variables['parcels'] ?? 0),
|
|
66
|
+
'entities' => (int) ($variables['entities'] ?? 0),
|
|
67
|
+
'base_fee' => (float) ($variables['base_fee'] ?? 0),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
return array_merge($normalized, $variables);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
protected static function validationFixture(): array
|
|
74
|
+
{
|
|
75
|
+
return static::normalizeVariables([
|
|
76
|
+
'distance_m' => 25000,
|
|
77
|
+
'time_s' => 5400,
|
|
78
|
+
'stops' => 4,
|
|
79
|
+
'waypoints' => 2,
|
|
80
|
+
'parcels' => 3,
|
|
81
|
+
'entities' => 5,
|
|
82
|
+
'base_fee' => 100,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
protected static function evaluateFunctions(string $algorithm): string
|
|
87
|
+
{
|
|
88
|
+
while (preg_match(static::FUNCTION_PATTERN, $algorithm)) {
|
|
89
|
+
$algorithm = preg_replace_callback(static::FUNCTION_PATTERN, function ($matches) {
|
|
90
|
+
$function = $matches[1];
|
|
91
|
+
$args = array_map('trim', explode(',', $matches[2]));
|
|
92
|
+
$m = new EvalMath();
|
|
93
|
+
$m->suppress_errors = true;
|
|
94
|
+
$numbers = array_map(function ($arg) use ($m) {
|
|
95
|
+
$result = $m->evaluate($arg);
|
|
96
|
+
|
|
97
|
+
if ($result === false || $result === null || !is_numeric($result)) {
|
|
98
|
+
return (float) $arg;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (float) $result;
|
|
102
|
+
}, $args);
|
|
103
|
+
|
|
104
|
+
return match ($function) {
|
|
105
|
+
'max' => (string) max($numbers[0] ?? 0, $numbers[1] ?? 0),
|
|
106
|
+
'min' => (string) min($numbers[0] ?? 0, $numbers[1] ?? 0),
|
|
107
|
+
'ceil' => (string) ceil($numbers[0] ?? 0),
|
|
108
|
+
'floor' => (string) floor($numbers[0] ?? 0),
|
|
109
|
+
'round' => (string) round($numbers[0] ?? 0, (int) ($numbers[1] ?? 0)),
|
|
110
|
+
default => $matches[0],
|
|
111
|
+
};
|
|
112
|
+
}, $algorithm);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return $algorithm;
|
|
116
|
+
}
|
|
31
117
|
}
|