@fleetbase/fleetops-engine 0.6.54 → 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 (220) hide show
  1. package/addon/components/admin/map-settings.hbs +1 -1
  2. package/addon/components/admin/routing-settings.hbs +2 -1
  3. package/addon/components/cell/attached-vehicle.hbs +10 -0
  4. package/addon/components/cell/attached-vehicle.js +58 -0
  5. package/addon/components/cell/device-identity.hbs +24 -0
  6. package/addon/components/cell/device-identity.js +129 -0
  7. package/addon/components/cell/driver-identity.hbs +33 -0
  8. package/addon/components/cell/driver-identity.js +143 -0
  9. package/addon/components/cell/equipment-identity.hbs +5 -0
  10. package/addon/components/cell/equipment-identity.js +48 -0
  11. package/addon/components/cell/part-identity.hbs +5 -0
  12. package/addon/components/cell/part-identity.js +63 -0
  13. package/addon/components/cell/telematic-device.hbs +38 -0
  14. package/addon/components/cell/telematic-device.js +44 -0
  15. package/addon/components/cell/telematic-provider.hbs +33 -16
  16. package/addon/components/cell/telematic-provider.js +50 -8
  17. package/addon/components/cell/vehicle-identity.hbs +33 -0
  18. package/addon/components/cell/vehicle-identity.js +138 -0
  19. package/addon/components/device/details.js +1 -1
  20. package/addon/components/device/form.hbs +2 -2
  21. package/addon/components/device/form.js +7 -0
  22. package/addon/components/device/panel-header.hbs +45 -20
  23. package/addon/components/device/panel-header.js +49 -1
  24. package/addon/components/device/panel-tabs/events.hbs +30 -0
  25. package/addon/components/device/panel-tabs/events.js +128 -0
  26. package/addon/components/device/panel-tabs/sensors.hbs +30 -0
  27. package/addon/components/device/panel-tabs/sensors.js +93 -0
  28. package/addon/components/device/panel-tabs/vehicle.hbs +68 -0
  29. package/addon/components/device/panel-tabs/vehicle.js +90 -0
  30. package/addon/components/device-event/details.hbs +192 -0
  31. package/addon/components/device-event/details.js +151 -0
  32. package/addon/components/driver/schedule.js +1 -1
  33. package/addon/components/equipment/form.js +8 -1
  34. package/addon/components/layout/fleet-ops-sidebar.hbs +1 -1
  35. package/addon/components/layout/fleet-ops-sidebar.js +20 -6
  36. package/addon/components/map/drawer/device-event-listing.js +2 -0
  37. package/addon/components/map/drawer/driver-listing.js +2 -0
  38. package/addon/components/map/drawer/place-listing.js +2 -0
  39. package/addon/components/map/drawer/position-listing.hbs +11 -6
  40. package/addon/components/map/drawer/position-listing.js +17 -4
  41. package/addon/components/map/drawer/vehicle-listing.js +2 -0
  42. package/addon/components/sensor/panel-header.hbs +47 -15
  43. package/addon/components/sensor/panel-header.js +59 -1
  44. package/addon/components/table/cell/dropdown/action-item.hbs +31 -0
  45. package/addon/components/table/cell/dropdown/action-item.js +62 -0
  46. package/addon/components/table/cell/dropdown.hbs +28 -0
  47. package/addon/components/table/cell/dropdown.js +91 -0
  48. package/addon/components/telematic/details.hbs +1 -1
  49. package/addon/components/telematic/form.hbs +2 -2
  50. package/addon/components/telematic/settings.hbs +1 -1
  51. package/addon/components/vendor/panel-header.hbs +82 -0
  52. package/addon/components/vendor/panel-header.js +48 -0
  53. package/addon/controllers/connectivity/devices/index/details/events.js +1 -1
  54. package/addon/controllers/connectivity/devices/index/details/vehicle.js +50 -2
  55. package/addon/controllers/connectivity/devices/index.js +144 -129
  56. package/addon/controllers/connectivity/events/details.js +70 -0
  57. package/addon/controllers/connectivity/events/index.js +187 -11
  58. package/addon/controllers/connectivity/sensors/index.js +160 -14
  59. package/addon/controllers/connectivity/telematics/details/devices.js +126 -134
  60. package/addon/controllers/connectivity/telematics/details/events.js +1 -1
  61. package/addon/controllers/connectivity/telematics/details/sensors.js +4 -0
  62. package/addon/controllers/maintenance/equipment/index.js +1 -2
  63. package/addon/controllers/maintenance/parts/index.js +1 -2
  64. package/addon/controllers/management/drivers/index.js +82 -42
  65. package/addon/controllers/management/fuel-reports/index.js +63 -4
  66. package/addon/controllers/management/issues/index.js +63 -4
  67. package/addon/controllers/management/vehicles/index.js +33 -6
  68. package/addon/controllers/settings/map.js +15 -0
  69. package/addon/instance-initializers/register-vroom-allocation.js +1 -1
  70. package/addon/routes/connectivity/devices/index/details/vehicle.js +24 -2
  71. package/addon/routes/connectivity/devices/index.js +7 -0
  72. package/addon/routes/connectivity/events/details.js +28 -0
  73. package/addon/routes/connectivity/events/index.js +6 -2
  74. package/addon/routes/connectivity/sensors/index.js +7 -2
  75. package/addon/routes/connectivity/telematics/details/devices.js +2 -0
  76. package/addon/routes/management/vehicles/index/details.js +3 -1
  77. package/addon/services/device-actions.js +30 -0
  78. package/addon/services/device-event-actions.js +1 -1
  79. package/addon/services/equipment-actions.js +1 -0
  80. package/addon/services/map-adapter/google.js +25 -0
  81. package/addon/services/map-adapter/leaflet.js +1 -1
  82. package/addon/services/map-adapter-interface.js +1 -1
  83. package/addon/services/map-manager.js +4 -1
  84. package/addon/services/map-settings.js +15 -0
  85. package/addon/services/sensor-actions.js +3 -0
  86. package/addon/services/service-rate-actions.js +39 -1
  87. package/addon/services/vehicle-actions.js +24 -12
  88. package/addon/services/vendor-actions.js +3 -0
  89. package/addon/styles/fleetops-engine.css +9 -1
  90. package/addon/templates/connectivity/devices/index/details/vehicle.hbs +66 -2
  91. package/addon/templates/connectivity/events/details.hbs +40 -0
  92. package/addon/templates/management/vendors/index/details.hbs +2 -2
  93. package/addon/templates/settings/map.hbs +38 -9
  94. package/addon/templates/settings/notifications.hbs +2 -1
  95. package/addon/templates/settings/routing.hbs +1 -0
  96. package/addon/utils/device-table-columns.js +288 -0
  97. package/addon/utils/identity-cell-resource.js +20 -0
  98. package/addon/utils/leaflet-plugin-loader.js +5 -5
  99. package/addon/utils/map-drawer-dropdown-position.js +47 -0
  100. package/addon/utils/route-colors.js +1 -1
  101. package/addon/utils/trackable-option.js +93 -0
  102. package/app/components/cell/attached-vehicle.js +1 -0
  103. package/app/components/cell/device-identity.js +1 -0
  104. package/app/components/cell/driver-identity.js +1 -0
  105. package/app/components/cell/equipment-identity.js +1 -0
  106. package/app/components/cell/part-identity.js +1 -0
  107. package/app/components/cell/telematic-device.js +1 -0
  108. package/app/components/cell/vehicle-identity.js +1 -0
  109. package/app/components/device/panel-tabs/events.hbs +30 -0
  110. package/app/components/device/panel-tabs/events.js +128 -0
  111. package/app/components/device/panel-tabs/sensors.hbs +30 -0
  112. package/app/components/device/panel-tabs/sensors.js +93 -0
  113. package/app/components/device/panel-tabs/vehicle.hbs +68 -0
  114. package/app/components/device/panel-tabs/vehicle.js +90 -0
  115. package/app/components/device-event/details.hbs +192 -0
  116. package/app/components/device-event/details.js +1 -0
  117. package/app/components/table/cell/dropdown/action-item.js +1 -0
  118. package/app/components/table/cell/dropdown.js +1 -0
  119. package/app/components/vendor/panel-header.js +1 -0
  120. package/app/controllers/connectivity/events/{index/details.js → details.js} +1 -1
  121. package/app/routes/connectivity/events/{index/details.js → details.js} +1 -1
  122. package/app/templates/connectivity/events/{index/details.js → details.js} +1 -1
  123. package/app/utils/map-drawer-dropdown-position.js +1 -0
  124. package/app/utils/trackable-option.js +1 -0
  125. package/composer.json +1 -1
  126. package/extension.json +1 -1
  127. package/package.json +4 -4
  128. package/server/migrations/2026_06_23_000001_delete_bad_afaqy_telematics_sensors.php +34 -0
  129. package/server/src/Console/Commands/SyncTelematics.php +4 -4
  130. package/server/src/Exports/ContactExport.php +12 -5
  131. package/server/src/Exports/DeviceExport.php +92 -0
  132. package/server/src/Exports/DriverExport.php +28 -3
  133. package/server/src/Exports/EquipmentExport.php +94 -0
  134. package/server/src/Exports/FleetExport.php +22 -7
  135. package/server/src/Exports/FuelReportExport.php +16 -1
  136. package/server/src/Exports/IssueExport.php +25 -3
  137. package/server/src/Exports/MaintenanceExport.php +106 -0
  138. package/server/src/Exports/MaintenanceScheduleExport.php +99 -0
  139. package/server/src/Exports/OrderExport.php +24 -5
  140. package/server/src/Exports/PartExport.php +99 -0
  141. package/server/src/Exports/PlaceExport.php +33 -7
  142. package/server/src/Exports/SensorExport.php +94 -0
  143. package/server/src/Exports/ServiceAreaExport.php +31 -7
  144. package/server/src/Exports/ServiceRateExport.php +41 -1
  145. package/server/src/Exports/TelematicExport.php +90 -0
  146. package/server/src/Exports/VehicleExport.php +274 -8
  147. package/server/src/Exports/VendorExport.php +15 -2
  148. package/server/src/Exports/WorkOrderExport.php +92 -0
  149. package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +176 -0
  150. package/server/src/Http/Controllers/Api/v1/DeviceController.php +225 -0
  151. package/server/src/Http/Controllers/Api/v1/EquipmentController.php +114 -0
  152. package/server/src/Http/Controllers/Api/v1/FuelTransactionController.php +185 -0
  153. package/server/src/Http/Controllers/Api/v1/PartController.php +120 -0
  154. package/server/src/Http/Controllers/Api/v1/SensorController.php +132 -0
  155. package/server/src/Http/Controllers/Api/v1/WorkOrderController.php +161 -0
  156. package/server/src/Http/Controllers/Internal/v1/DeviceController.php +36 -1
  157. package/server/src/Http/Controllers/Internal/v1/DeviceEventController.php +1 -1
  158. package/server/src/Http/Controllers/Internal/v1/EquipmentController.php +17 -0
  159. package/server/src/Http/Controllers/Internal/v1/LiveController.php +1 -1
  160. package/server/src/Http/Controllers/Internal/v1/MaintenanceController.php +17 -0
  161. package/server/src/Http/Controllers/Internal/v1/MaintenanceScheduleController.php +17 -0
  162. package/server/src/Http/Controllers/Internal/v1/PartController.php +17 -0
  163. package/server/src/Http/Controllers/Internal/v1/SensorController.php +18 -0
  164. package/server/src/Http/Controllers/Internal/v1/ServiceRateController.php +82 -13
  165. package/server/src/Http/Controllers/Internal/v1/SettingController.php +29 -5
  166. package/server/src/Http/Controllers/Internal/v1/TelematicController.php +18 -0
  167. package/server/src/Http/Controllers/Internal/v1/WorkOrderController.php +17 -0
  168. package/server/src/Http/Filter/DeviceEventFilter.php +21 -0
  169. package/server/src/Http/Filter/DeviceFilter.php +50 -4
  170. package/server/src/Http/Filter/EquipmentFilter.php +23 -0
  171. package/server/src/Http/Filter/FuelProviderTransactionFilter.php +57 -2
  172. package/server/src/Http/Filter/PartFilter.php +47 -0
  173. package/server/src/Http/Filter/SensorFilter.php +34 -6
  174. package/server/src/Http/Filter/WorkOrderFilter.php +23 -0
  175. package/server/src/Http/Requests/CreateDeviceRequest.php +51 -0
  176. package/server/src/Http/Requests/CreateEquipmentRequest.php +35 -0
  177. package/server/src/Http/Requests/CreateFuelTransactionRequest.php +50 -0
  178. package/server/src/Http/Requests/CreatePartRequest.php +40 -0
  179. package/server/src/Http/Requests/CreateSensorRequest.php +47 -0
  180. package/server/src/Http/Requests/CreateWorkOrderRequest.php +42 -0
  181. package/server/src/Http/Requests/UpdateDeviceRequest.php +7 -0
  182. package/server/src/Http/Requests/UpdateEquipmentRequest.php +7 -0
  183. package/server/src/Http/Requests/UpdateFuelTransactionRequest.php +7 -0
  184. package/server/src/Http/Requests/UpdatePartRequest.php +7 -0
  185. package/server/src/Http/Requests/UpdateSensorRequest.php +7 -0
  186. package/server/src/Http/Requests/UpdateWorkOrderRequest.php +7 -0
  187. package/server/src/Http/Resources/v1/Device.php +72 -0
  188. package/server/src/Http/Resources/v1/Equipment.php +57 -0
  189. package/server/src/Http/Resources/v1/FuelTransaction.php +69 -0
  190. package/server/src/Http/Resources/v1/Index/Vehicle.php +19 -0
  191. package/server/src/Http/Resources/v1/Part.php +64 -0
  192. package/server/src/Http/Resources/v1/Sensor.php +68 -0
  193. package/server/src/Jobs/SyncTelematicDevicesJob.php +194 -38
  194. package/server/src/Models/Device.php +2 -0
  195. package/server/src/Models/DeviceEvent.php +81 -0
  196. package/server/src/Models/Equipment.php +55 -0
  197. package/server/src/Models/Order.php +2 -1
  198. package/server/src/Observers/PurchaseRateObserver.php +2 -1
  199. package/server/src/Support/Analytics/RevenueTrend.php +3 -8
  200. package/server/src/Support/Metrics/ActiveRevenueQuery.php +121 -0
  201. package/server/src/Support/Metrics/AvgOrderValueMetric.php +1 -7
  202. package/server/src/Support/Metrics/EarningsMetric.php +1 -10
  203. package/server/src/Support/Telematics/Providers/AfaqyProvider.php +105 -5
  204. package/server/src/Support/Telematics/Providers/FlespiProvider.php +89 -15
  205. package/server/src/Support/Telematics/Providers/GeotabProvider.php +123 -35
  206. package/server/src/Support/Telematics/Providers/SafeeProvider.php +567 -47
  207. package/server/src/Support/Telematics/Providers/SamsaraProvider.php +122 -14
  208. package/server/src/Support/Telematics/TelematicService.php +28 -9
  209. package/server/src/routes.php +67 -15
  210. package/server/tests/DeviceFilterTest.php +9 -0
  211. package/server/tests/DriverVehicleStatusDefaultTest.php +19 -2
  212. package/server/tests/EquipmentAssignmentMorphTypeTest.php +35 -0
  213. package/server/tests/ExportCoverageTest.php +79 -0
  214. package/server/tests/MetricsRegistryTest.php +45 -5
  215. package/server/tests/PublicFleetOpsRestEndpointsTest.php +191 -0
  216. package/server/tests/ServiceRateRouteValidationTest.php +101 -0
  217. package/server/tests/TelematicsHardeningTest.php +873 -7
  218. package/addon/controllers/connectivity/events/index/details.js +0 -3
  219. package/addon/routes/connectivity/events/index/details.js +0 -3
  220. package/addon/templates/connectivity/events/index/details.hbs +0 -1
@@ -18,6 +18,7 @@ use Fleetbase\Traits\TracksApiCredential;
18
18
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
19
  use Illuminate\Database\Eloquent\Relations\HasMany;
20
20
  use Illuminate\Database\Eloquent\Relations\MorphTo;
21
+ use Illuminate\Database\Eloquent\Relations\Relation;
21
22
  use Spatie\Activitylog\LogOptions;
22
23
  use Spatie\Activitylog\Traits\LogsActivity;
23
24
  use Spatie\Sluggable\HasSlug;
@@ -94,6 +95,15 @@ class Equipment extends Model
94
95
  'slug',
95
96
  ];
96
97
 
98
+ /**
99
+ * Set attributes and defaults.
100
+ *
101
+ * @var array
102
+ */
103
+ protected $attributes = [
104
+ 'status' => 'available',
105
+ ];
106
+
97
107
  /**
98
108
  * Dynamic attributes that are appended to object.
99
109
  *
@@ -148,6 +158,19 @@ class Equipment extends Model
148
158
  */
149
159
  protected static $logName = 'equipment';
150
160
 
161
+ /**
162
+ * Enforce assignment morph aliases for existing short type values.
163
+ */
164
+ public static function boot(): void
165
+ {
166
+ parent::boot();
167
+
168
+ Relation::morphMap([
169
+ 'fleet-ops:vehicle' => Vehicle::class,
170
+ 'fleet-ops:driver' => Driver::class,
171
+ ]);
172
+ }
173
+
151
174
  /**
152
175
  * Get the options for generating the slug.
153
176
  */
@@ -192,6 +215,38 @@ class Equipment extends Model
192
215
  return $this->morphTo(__FUNCTION__, 'equipable_type', 'equipable_uuid');
193
216
  }
194
217
 
218
+ /**
219
+ * Set the equipment status attribute.
220
+ */
221
+ public function setStatusAttribute(?string $status = 'available'): void
222
+ {
223
+ $this->attributes['status'] = $status === null || $status === 'active' ? 'available' : $status;
224
+ }
225
+
226
+ /**
227
+ * Normalize equipment assignment type aliases to Eloquent morph classes.
228
+ */
229
+ public function setEquipableTypeAttribute(?string $type): void
230
+ {
231
+ if ($type === null || $type === '') {
232
+ $this->attributes['equipable_type'] = null;
233
+
234
+ return;
235
+ }
236
+
237
+ if (str_contains($type, '\\')) {
238
+ $this->attributes['equipable_type'] = $type;
239
+
240
+ return;
241
+ }
242
+
243
+ $this->attributes['equipable_type'] = match ($type) {
244
+ 'fleet-ops:vehicle', 'vehicle' => Utils::getMutationType('fleet-ops:vehicle'),
245
+ 'fleet-ops:driver', 'driver' => Utils::getMutationType('fleet-ops:driver'),
246
+ default => $type,
247
+ };
248
+ }
249
+
195
250
  public function maintenances(): HasMany
196
251
  {
197
252
  return $this->hasMany(Maintenance::class, 'maintainable_uuid', 'uuid')
@@ -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
@@ -151,6 +151,7 @@ class AfaqyProvider extends AbstractProvider
151
151
 
152
152
  return [
153
153
  'device_id' => $payload['_id'] ?? $payload['id'] ?? null,
154
+ 'external_id' => $payload['_id'] ?? $payload['id'] ?? null,
154
155
  'name' => $payload['name'] ?? data_get($payload, 'profile.plate_number') ?? 'Unknown Unit',
155
156
  'provider' => 'afaqy',
156
157
  'model' => data_get($payload, 'profile.model') ?? $payload['device'] ?? null,
@@ -210,12 +211,35 @@ class AfaqyProvider extends AbstractProvider
210
211
 
211
212
  public function normalizeSensor(array $payload): array
212
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
+
213
226
  return [
214
- 'sensor_type' => $payload['type'] ?? $payload['name'] ?? $payload['param'] ?? 'generic',
215
- 'value' => $payload['value'] ?? $payload['last_val']['value'] ?? $payload['last_update'] ?? null,
216
- 'unit' => $payload['units'] ?? null,
217
- 'recorded_at' => $this->parseTimestamp($payload['updated_at'] ?? $payload['created_at'] ?? null),
218
- '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),
219
243
  ];
220
244
  }
221
245
 
@@ -476,4 +500,80 @@ class AfaqyProvider extends AbstractProvider
476
500
  ?? data_get($payload, 'fc.level')
477
501
  ?? null;
478
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
+ }
479
579
  }
@@ -2,6 +2,8 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Support\Telematics\Providers;
4
4
 
5
+ use Illuminate\Support\Carbon;
6
+
5
7
  /**
6
8
  * Class FlespiProvider.
7
9
  *
@@ -77,34 +79,71 @@ class FlespiProvider extends AbstractProvider
77
79
 
78
80
  public function normalizeDevice(array $payload): array
79
81
  {
82
+ $telemetry = $payload['telemetry'] ?? $payload;
83
+ $externalId = $payload['id'] ?? $payload['device.id'] ?? null;
84
+ $occurredAt = $this->parseTimestamp($telemetry['timestamp'] ?? $payload['last_active'] ?? null);
85
+
80
86
  return [
81
- 'external_id' => $payload['id'],
82
- 'device_name' => $payload['name'] ?? 'Unknown Device',
83
- 'device_provider' => 'flespi',
84
- 'device_model' => $payload['device_type_id'] ?? null,
85
- 'imei' => $payload['configuration']['ident'] ?? null,
86
- 'phone' => $payload['configuration']['phone'] ?? null,
87
- 'status' => isset($payload['telemetry']) ? 'active' : 'inactive',
88
- 'location' => [
89
- 'lat' => $payload['telemetry']['position.latitude'] ?? null,
90
- 'lng' => $payload['telemetry']['position.longitude'] ?? null,
87
+ 'device_id' => $externalId,
88
+ 'external_id' => $externalId,
89
+ 'name' => $payload['name'] ?? $telemetry['device.name'] ?? 'Unknown Device',
90
+ 'provider' => 'flespi',
91
+ 'model' => $payload['device_type_id'] ?? $payload['device_type_name'] ?? null,
92
+ 'imei' => $payload['configuration']['ident'] ?? $payload['ident'] ?? $telemetry['ident'] ?? null,
93
+ 'serial_number' => $payload['configuration']['serial'] ?? $payload['serial'] ?? null,
94
+ 'phone' => $payload['configuration']['phone'] ?? null,
95
+ 'status' => isset($payload['telemetry']) ? 'active' : 'inactive',
96
+ 'online' => $this->resolveOnline($telemetry),
97
+ 'last_seen_at' => $occurredAt,
98
+ 'location' => [
99
+ 'lat' => $telemetry['position.latitude'] ?? null,
100
+ 'lng' => $telemetry['position.longitude'] ?? null,
101
+ ],
102
+ 'speed' => $telemetry['position.speed'] ?? $telemetry['vehicle.speed'] ?? null,
103
+ 'heading' => $telemetry['position.direction'] ?? $telemetry['position.heading'] ?? null,
104
+ 'altitude' => $telemetry['position.altitude'] ?? null,
105
+ 'odometer' => $telemetry['vehicle.mileage'] ?? $telemetry['vehicle.odometer'] ?? null,
106
+ 'ignition' => $this->extractIgnition($telemetry),
107
+ 'fuel_level' => $telemetry['fuel.level'] ?? $telemetry['can.fuel.level'] ?? null,
108
+ 'meta' => [
109
+ 'raw' => $payload,
110
+ 'provider_status' => array_filter([
111
+ 'status' => $payload['status'] ?? $telemetry['status'] ?? null,
112
+ 'online' => $telemetry['online'] ?? null,
113
+ 'connected' => $telemetry['device.connected'] ?? $telemetry['connected'] ?? null,
114
+ ], fn ($value) => $value !== null),
91
115
  ],
92
- 'meta' => $payload,
93
116
  ];
94
117
  }
95
118
 
96
119
  public function normalizeEvent(array $payload): array
97
120
  {
121
+ $deviceId = $payload['device.id'] ?? $payload['id'] ?? null;
122
+
98
123
  return [
99
- 'external_id' => $payload['id'] ?? null,
100
- 'device_id' => $payload['device.id'] ?? null,
124
+ 'external_id' => $payload['id'] ?? $deviceId,
125
+ 'device_id' => $deviceId,
101
126
  'event_type' => $payload['event.enum'] ?? 'telemetry_update',
102
- 'occurred_at' => isset($payload['timestamp']) ? date('Y-m-d H:i:s', $payload['timestamp']) : now(),
127
+ 'occurred_at' => $this->parseTimestamp($payload['timestamp'] ?? null) ?? now(),
128
+ 'online' => $this->resolveOnline($payload),
103
129
  'location' => [
104
130
  'lat' => $payload['position.latitude'] ?? null,
105
131
  'lng' => $payload['position.longitude'] ?? null,
106
132
  ],
107
- 'meta' => $payload,
133
+ 'speed' => $payload['position.speed'] ?? $payload['vehicle.speed'] ?? null,
134
+ 'heading' => $payload['position.direction'] ?? $payload['position.heading'] ?? null,
135
+ 'altitude' => $payload['position.altitude'] ?? null,
136
+ 'odometer' => $payload['vehicle.mileage'] ?? $payload['vehicle.odometer'] ?? null,
137
+ 'ignition' => $this->extractIgnition($payload),
138
+ 'fuel_level' => $payload['fuel.level'] ?? $payload['can.fuel.level'] ?? null,
139
+ 'meta' => [
140
+ 'raw' => $payload,
141
+ 'provider_status' => array_filter([
142
+ 'status' => $payload['status'] ?? null,
143
+ 'online' => $payload['online'] ?? null,
144
+ 'connected' => $payload['device.connected'] ?? $payload['connected'] ?? null,
145
+ ], fn ($value) => $value !== null),
146
+ ],
108
147
  ];
109
148
  }
110
149
 
@@ -180,4 +219,39 @@ class FlespiProvider extends AbstractProvider
180
219
  {
181
220
  return true;
182
221
  }
222
+
223
+ protected function parseTimestamp($value): ?string
224
+ {
225
+ if (!$value) {
226
+ return null;
227
+ }
228
+
229
+ if (is_numeric($value)) {
230
+ return Carbon::createFromTimestamp((float) $value)->toDateTimeString();
231
+ }
232
+
233
+ return Carbon::parse($value)->toDateTimeString();
234
+ }
235
+
236
+ protected function resolveOnline(array $payload): ?bool
237
+ {
238
+ $value = $payload['online'] ?? $payload['device.connected'] ?? $payload['connected'] ?? null;
239
+
240
+ if ($value === null) {
241
+ return isset($payload['timestamp']);
242
+ }
243
+
244
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
245
+ }
246
+
247
+ protected function extractIgnition(array $payload): ?bool
248
+ {
249
+ $value = $payload['engine.ignition.status'] ?? $payload['ignition.status'] ?? null;
250
+
251
+ if ($value === null) {
252
+ return null;
253
+ }
254
+
255
+ return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $value;
256
+ }
183
257
  }