@fleetbase/fleetops-engine 0.6.55 → 0.6.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/device/details.js +1 -1
  3. package/addon/components/device-event/details.hbs +192 -0
  4. package/addon/components/device-event/details.js +151 -0
  5. package/addon/components/driver/schedule.js +1 -1
  6. package/addon/components/layout/fleet-ops-sidebar.js +15 -9
  7. package/addon/components/telematic/details.hbs +1 -1
  8. package/addon/components/telematic/form.hbs +2 -2
  9. package/addon/components/telematic/settings.hbs +1 -1
  10. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  11. package/addon/controllers/connectivity/devices/index/details/vehicle.js +2 -2
  12. package/addon/controllers/connectivity/events/details.js +68 -1
  13. package/addon/controllers/connectivity/sensors/index.js +4 -0
  14. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  15. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  16. package/addon/controllers/settings/map.js +15 -0
  17. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  18. package/addon/routes/connectivity/events/details.js +26 -1
  19. package/addon/routes/management/vehicles/index/details.js +3 -1
  20. package/addon/services/map-adapter/google.js +25 -0
  21. package/addon/services/map-adapter/leaflet.js +1 -1
  22. package/addon/services/map-adapter-interface.js +1 -1
  23. package/addon/services/map-manager.js +4 -1
  24. package/addon/services/map-settings.js +15 -0
  25. package/addon/styles/fleetops-engine.css +1 -1
  26. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +2 -1
  27. package/addon/templates/connectivity/events/details.hbs +40 -1
  28. package/addon/templates/settings/map.hbs +38 -9
  29. package/addon/utils/leaflet-plugin-loader.js +5 -5
  30. package/addon/utils/route-colors.js +1 -1
  31. package/app/components/device-event/details.hbs +192 -0
  32. package/app/components/device-event/details.js +1 -0
  33. package/composer.json +1 -1
  34. package/extension.json +1 -1
  35. package/package.json +2 -2
  36. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  37. package/server/src/Exports/ContactExport.php +12 -5
  38. package/server/src/Exports/DeviceExport.php +92 -0
  39. package/server/src/Exports/DriverExport.php +28 -3
  40. package/server/src/Exports/EquipmentExport.php +94 -0
  41. package/server/src/Exports/FleetExport.php +22 -7
  42. package/server/src/Exports/FuelReportExport.php +16 -1
  43. package/server/src/Exports/IssueExport.php +25 -3
  44. package/server/src/Exports/MaintenanceExport.php +106 -0
  45. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  46. package/server/src/Exports/OrderExport.php +24 -5
  47. package/server/src/Exports/PartExport.php +99 -0
  48. package/server/src/Exports/PlaceExport.php +33 -7
  49. package/server/src/Exports/SensorExport.php +94 -0
  50. package/server/src/Exports/ServiceAreaExport.php +31 -7
  51. package/server/src/Exports/ServiceRateExport.php +41 -1
  52. package/server/src/Exports/TelematicExport.php +90 -0
  53. package/server/src/Exports/VehicleExport.php +274 -8
  54. package/server/src/Exports/VendorExport.php +15 -2
  55. package/server/src/Exports/WorkOrderExport.php +92 -0
  56. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  57. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  58. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  59. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  60. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  61. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  62. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  63. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +18 -0
  64. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  65. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  66. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  67. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  68. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  69. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  70. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  71. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  72. package/server/src/Http/Filter/DeviceFilter.php +33 -10
  73. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  74. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  75. package/server/src/Http/Filter/PartFilter.php +47 -0
  76. package/server/src/Http/Filter/SensorFilter.php +34 -6
  77. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  78. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  79. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  80. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  81. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  82. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  83. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  84. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  85. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  86. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  87. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  88. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  89. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  90. package/server/src/Http/Resources/v1/Device.php +72 -0
  91. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  92. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  93. package/server/src/Http/Resources/v1/Part.php +64 -0
  94. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  95. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  96. package/server/src/Models/Order.php +2 -1
  97. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  98. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  99. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  100. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  101. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  102. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +104 -5
  103. package/server/src/Support/Telematics/Providers/SafeeProvider.php +550 -54
  104. package/server/src/Support/Telematics/TelematicService.php +28 -9
  105. package/server/src/routes.php +67 -14
  106. package/server/tests/ExportCoverageTest.php +79 -0
  107. package/server/tests/MetricsRegistryTest.php +45 -5
  108. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  109. package/server/tests/TelematicsHardeningTest.php +636 -1
@@ -1208,7 +1208,8 @@ class Order extends Model
1208
1208
  'description' => 'Dispatch order',
1209
1209
  'type' => 'dispatch',
1210
1210
  'direction' => Transaction::DIRECTION_CREDIT,
1211
- 'status' => 'success',
1211
+ 'status' => Transaction::STATUS_SUCCESS,
1212
+ 'settlement_status' => Transaction::SETTLEMENT_STATUS_UNPAID,
1212
1213
  ]);
1213
1214
 
1214
1215
  $this->updateQuietly(['transaction_uuid' => $transaction->uuid]);
@@ -49,7 +49,8 @@ class PurchaseRateObserver
49
49
  'description' => 'Dispatch order',
50
50
  'type' => 'dispatch',
51
51
  'direction' => Transaction::DIRECTION_CREDIT,
52
- 'status' => 'success',
52
+ 'status' => Transaction::STATUS_SUCCESS,
53
+ 'settlement_status' => Transaction::SETTLEMENT_STATUS_UNPAID,
53
54
  ]);
54
55
 
55
56
  if (isset($purchaseRate->serviceQuote)) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Analytics;
4
4
 
5
- use Fleetbase\Models\Transaction;
5
+ use Fleetbase\FleetOps\Support\Metrics\ActiveRevenueQuery;
6
6
  use Illuminate\Support\Carbon;
7
7
 
8
8
  /**
@@ -32,9 +32,7 @@ class RevenueTrend extends AbstractAnalytics
32
32
  default => '%Y-%m-%d',
33
33
  };
34
34
 
35
- $rows = Transaction::where('company_uuid', $this->company->uuid)
36
- ->where('currency', $currency)
37
- ->whereBetween('created_at', [$start, $end])
35
+ $rows = ActiveRevenueQuery::forCompany($this->company, $currency, $start, $end)
38
36
  ->selectRaw('DATE_FORMAT(created_at, ?) as bucket, SUM(amount) as total', [$format])
39
37
  ->groupBy('bucket')
40
38
  ->orderBy('bucket')
@@ -47,10 +45,7 @@ class RevenueTrend extends AbstractAnalytics
47
45
  $duration = $end->getTimestamp() - $start->getTimestamp();
48
46
  $compareStart = (clone $start);
49
47
  $compareStart = (new \DateTime())->setTimestamp($start->getTimestamp() - $duration);
50
- $previousTotal = (float) Transaction::where('company_uuid', $this->company->uuid)
51
- ->where('currency', $currency)
52
- ->whereBetween('created_at', [$compareStart, $start])
53
- ->sum('amount');
48
+ $previousTotal = (float) ActiveRevenueQuery::forCompany($this->company, $currency, $compareStart, $start)->sum('amount');
54
49
 
55
50
  $deltaPct = $previousTotal > 0
56
51
  ? round((($total - $previousTotal) / $previousTotal) * 100, 1)
@@ -0,0 +1,121 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Support\Metrics;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\Models\Company;
7
+ use Fleetbase\Models\Transaction;
8
+ use Illuminate\Database\Eloquent\Builder;
9
+ use Illuminate\Support\Facades\Schema;
10
+
11
+ class ActiveRevenueQuery
12
+ {
13
+ public const ACTIVE_STATUSES = [Transaction::STATUS_SUCCESS];
14
+ public const INACTIVE_TRANSACTION_STATUSES = ['pending', 'failed', 'cancelled', 'canceled', 'void', 'voided', 'expired', 'reversed', 'refunded', 'ignored'];
15
+ public const INACTIVE_ORDER_STATUSES = ['canceled', 'cancelled', 'order_canceled'];
16
+ public const INACTIVE_INVOICE_STATUSES = ['void', 'voided', 'cancelled', 'canceled'];
17
+
18
+ public static function forCompany(Company $company, string $currency, ?\DateTimeInterface $start = null, ?\DateTimeInterface $end = null): Builder
19
+ {
20
+ $query = Transaction::query()
21
+ ->where('company_uuid', $company->uuid)
22
+ ->where('currency', $currency)
23
+ ->where('direction', Transaction::DIRECTION_CREDIT)
24
+ ->whereIn('status', self::ACTIVE_STATUSES)
25
+ ->whereNotIn('status', self::INACTIVE_TRANSACTION_STATUSES)
26
+ ->whereNull('deleted_at')
27
+ ->whereNull('voided_at')
28
+ ->whereNull('reversed_at')
29
+ ->whereNull('parent_transaction_uuid');
30
+
31
+ if ($start && $end) {
32
+ $query->whereBetween('created_at', [$start, $end]);
33
+ }
34
+
35
+ static::excludeInactiveOrders($query);
36
+ static::excludeInactiveInvoices($query);
37
+
38
+ return $query;
39
+ }
40
+
41
+ protected static function excludeInactiveOrders(Builder $query): void
42
+ {
43
+ if (!Schema::hasTable('orders')) {
44
+ return;
45
+ }
46
+
47
+ foreach (['subject_uuid' => 'subject_type', 'context_uuid' => 'context_type'] as $uuidColumn => $typeColumn) {
48
+ $query->whereNotExists(function ($subQuery) use ($uuidColumn, $typeColumn) {
49
+ $subQuery->selectRaw('1')
50
+ ->from('orders')
51
+ ->whereColumn('orders.uuid', "transactions.{$uuidColumn}")
52
+ ->where("transactions.{$typeColumn}", Order::class)
53
+ ->where(function ($orderQuery) {
54
+ static::inactiveOrderConstraint($orderQuery);
55
+ });
56
+ });
57
+ }
58
+
59
+ $query->whereNotExists(function ($subQuery) {
60
+ $subQuery->selectRaw('1')
61
+ ->from('orders')
62
+ ->whereColumn('orders.transaction_uuid', 'transactions.uuid')
63
+ ->where(function ($orderQuery) {
64
+ static::inactiveOrderConstraint($orderQuery);
65
+ });
66
+ });
67
+ }
68
+
69
+ protected static function excludeInactiveInvoices(Builder $query): void
70
+ {
71
+ $invoiceClass = 'Fleetbase\\Ledger\\Models\\Invoice';
72
+
73
+ if (!class_exists($invoiceClass) || !Schema::hasTable('ledger_invoices')) {
74
+ return;
75
+ }
76
+
77
+ foreach (['subject_uuid' => 'subject_type', 'context_uuid' => 'context_type'] as $uuidColumn => $typeColumn) {
78
+ $query->whereNotExists(function ($subQuery) use ($uuidColumn, $typeColumn, $invoiceClass) {
79
+ $subQuery->selectRaw('1')
80
+ ->from('ledger_invoices')
81
+ ->whereColumn('ledger_invoices.uuid', "transactions.{$uuidColumn}")
82
+ ->where("transactions.{$typeColumn}", $invoiceClass)
83
+ ->where(function ($invoiceQuery) {
84
+ static::inactiveInvoiceConstraint($invoiceQuery);
85
+ });
86
+ });
87
+ }
88
+
89
+ $query->whereNotExists(function ($subQuery) {
90
+ $subQuery->selectRaw('1')
91
+ ->from('ledger_invoices')
92
+ ->whereColumn('ledger_invoices.transaction_uuid', 'transactions.uuid')
93
+ ->where(function ($invoiceQuery) {
94
+ static::inactiveInvoiceConstraint($invoiceQuery);
95
+ });
96
+ });
97
+ }
98
+
99
+ protected static function inactiveOrderConstraint($query): void
100
+ {
101
+ $query->whereNotNull('orders.deleted_at')
102
+ ->orWhereIn('orders.status', self::INACTIVE_ORDER_STATUSES);
103
+ }
104
+
105
+ protected static function inactiveInvoiceConstraint($query): void
106
+ {
107
+ $query->whereNotNull('ledger_invoices.deleted_at')
108
+ ->orWhereIn('ledger_invoices.status', self::INACTIVE_INVOICE_STATUSES);
109
+
110
+ if (Schema::hasTable('orders')) {
111
+ $query->orWhereExists(function ($orderQuery) {
112
+ $orderQuery->selectRaw('1')
113
+ ->from('orders')
114
+ ->whereColumn('orders.uuid', 'ledger_invoices.order_uuid')
115
+ ->where(function ($inactiveOrderQuery) {
116
+ static::inactiveOrderConstraint($inactiveOrderQuery);
117
+ });
118
+ });
119
+ }
120
+ }
121
+ }
@@ -3,7 +3,6 @@
3
3
  namespace Fleetbase\FleetOps\Support\Metrics;
4
4
 
5
5
  use Fleetbase\FleetOps\Models\Order;
6
- use Fleetbase\Models\Transaction;
7
6
 
8
7
  /**
9
8
  * Average revenue per completed order in the period. Computed as
@@ -39,12 +38,7 @@ class AvgOrderValueMetric extends MoneyMetric
39
38
  return 0.0;
40
39
  }
41
40
 
42
- $revenueQuery = Transaction::where('company_uuid', $this->company->uuid)
43
- ->where('currency', $this->currency());
44
-
45
- if ($this->currentStart && $this->currentEnd) {
46
- $revenueQuery->whereBetween('created_at', [$this->currentStart, $this->currentEnd]);
47
- }
41
+ $revenueQuery = ActiveRevenueQuery::forCompany($this->company, $this->currency(), $this->currentStart, $this->currentEnd);
48
42
 
49
43
  $totalRevenue = (float) $revenueQuery->sum('amount');
50
44
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Metrics;
4
4
 
5
- use Fleetbase\Models\Transaction;
6
-
7
5
  class EarningsMetric extends MoneyMetric
8
6
  {
9
7
  public static function slug(): string
@@ -13,14 +11,7 @@ class EarningsMetric extends MoneyMetric
13
11
 
14
12
  protected function query(?\DateTimeInterface $start, ?\DateTimeInterface $end)
15
13
  {
16
- $query = Transaction::where('company_uuid', $this->company->uuid)
17
- ->where('currency', $this->currency());
18
-
19
- if ($start && $end) {
20
- $query->whereBetween('created_at', [$start, $end]);
21
- }
22
-
23
- return $query;
14
+ return ActiveRevenueQuery::forCompany($this->company, $this->currency(), $start, $end);
24
15
  }
25
16
 
26
17
  protected function aggregate($query): float
@@ -211,12 +211,35 @@ class AfaqyProvider extends AbstractProvider
211
211
 
212
212
  public function normalizeSensor(array $payload): array
213
213
  {
214
+ $unitId = $payload['device_id'] ?? $payload['unit_id'] ?? $payload['vehicle_id'] ?? null;
215
+ $sensorIdentity = $this->resolveSensorIdentity($payload);
216
+ $sensorName = $this->resolveSensorName($payload, $sensorIdentity);
217
+ $value = $this->resolveSensorValue($payload);
218
+
219
+ if (!$unitId || !$sensorIdentity || !$sensorName || !$this->isSensorValue($value)) {
220
+ throw new \InvalidArgumentException('AFAQY sensor payload does not contain a stable identity and scalar latest value.');
221
+ }
222
+
223
+ $type = $this->normalizeSensorType($payload, $sensorName);
224
+ $internalId = implode(':', ['afaqy', $unitId, $this->sensorIdentityPart($sensorIdentity)]);
225
+
214
226
  return [
215
- 'sensor_type' => $payload['type'] ?? $payload['name'] ?? $payload['param'] ?? 'generic',
216
- 'value' => $payload['value'] ?? $payload['last_val']['value'] ?? $payload['last_update'] ?? null,
217
- 'unit' => $payload['units'] ?? null,
218
- 'recorded_at' => $this->parseTimestamp($payload['updated_at'] ?? $payload['created_at'] ?? null),
219
- 'meta' => $payload,
227
+ 'internal_id' => $internalId,
228
+ 'external_id' => $internalId,
229
+ 'name' => $sensorName,
230
+ 'type' => $type,
231
+ 'sensor_type' => $type,
232
+ 'value' => $value,
233
+ 'unit' => $payload['unit'] ?? $payload['units'] ?? data_get($payload, 'last_val.unit'),
234
+ 'recorded_at' => $this->parseTimestamp(data_get($payload, 'last_val.dtt') ?? data_get($payload, 'last_update.dtt') ?? $payload['updated_at'] ?? $payload['created_at'] ?? null),
235
+ 'status' => 'active',
236
+ 'meta' => array_filter([
237
+ 'provider' => 'afaqy',
238
+ 'unit_id' => $unitId,
239
+ 'provider_sensor_id' => $sensorIdentity,
240
+ 'sensor_key' => $payload['sensor_key'] ?? null,
241
+ 'raw' => $payload,
242
+ ], fn ($value) => $value !== null),
220
243
  ];
221
244
  }
222
245
 
@@ -477,4 +500,80 @@ class AfaqyProvider extends AbstractProvider
477
500
  ?? data_get($payload, 'fc.level')
478
501
  ?? null;
479
502
  }
503
+
504
+ protected function resolveSensorIdentity(array $payload): ?string
505
+ {
506
+ $value = $payload['_id']
507
+ ?? $payload['id']
508
+ ?? $payload['sensor_id']
509
+ ?? $payload['ident']
510
+ ?? $payload['param']
511
+ ?? $payload['sensor_key']
512
+ ?? $payload['name']
513
+ ?? data_get($payload, 'sensor.name');
514
+
515
+ if ($value === null || $value === '') {
516
+ return null;
517
+ }
518
+
519
+ return (string) $value;
520
+ }
521
+
522
+ protected function resolveSensorName(array $payload, ?string $fallback = null): ?string
523
+ {
524
+ $value = $payload['name']
525
+ ?? data_get($payload, 'sensor.name')
526
+ ?? $payload['param']
527
+ ?? $payload['sensor_key']
528
+ ?? $fallback;
529
+
530
+ if ($value === null || $value === '') {
531
+ return null;
532
+ }
533
+
534
+ return (string) $value;
535
+ }
536
+
537
+ protected function resolveSensorValue(array $payload): mixed
538
+ {
539
+ if (array_key_exists('value', $payload)) {
540
+ return $payload['value'];
541
+ }
542
+
543
+ return data_get($payload, 'last_val.value')
544
+ ?? data_get($payload, 'last_update.value')
545
+ ?? data_get($payload, 'last_update.val')
546
+ ?? data_get($payload, 'last_update.state');
547
+ }
548
+
549
+ protected function isSensorValue(mixed $value): bool
550
+ {
551
+ return $value !== null && is_scalar($value);
552
+ }
553
+
554
+ protected function normalizeSensorType(array $payload, string $sensorName): string
555
+ {
556
+ $providerType = (string) ($payload['sensor_type'] ?? $payload['type'] ?? $payload['param'] ?? '');
557
+ $type = strtolower(trim($providerType . ' ' . $sensorName));
558
+
559
+ return match (true) {
560
+ str_contains($type, 'door') => 'door',
561
+ str_contains($type, 'temp') => 'temperature',
562
+ str_contains($type, 'humid') => 'humidity',
563
+ str_contains($type, 'fuel') => 'fuel',
564
+ str_starts_with($type, 'di') || str_contains($type, 'digital') => 'digital',
565
+ str_starts_with($type, 'ai') || str_contains($type, 'analog') => 'analog',
566
+ str_contains($type, 'battery') || str_contains($type, 'voltage') => 'voltage',
567
+ default => $this->sensorIdentityPart($type) ?: 'generic',
568
+ };
569
+ }
570
+
571
+ protected function sensorIdentityPart(string $value): string
572
+ {
573
+ $normalized = strtolower(trim($value));
574
+ $normalized = preg_replace('/[^a-z0-9]+/i', '_', $normalized) ?? '';
575
+ $normalized = trim($normalized, '_');
576
+
577
+ return $normalized !== '' ? $normalized : 'sensor';
578
+ }
480
579
  }