@fleetbase/fleetops-engine 0.6.44 → 0.6.46

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 (172) hide show
  1. package/addon/components/admin/map-settings.hbs +26 -0
  2. package/addon/components/admin/map-settings.js +46 -0
  3. package/addon/components/admin/routing-settings.hbs +11 -0
  4. package/addon/components/cell/order-route-type.hbs +46 -0
  5. package/addon/components/cell/order-route-type.js +90 -0
  6. package/addon/components/cell/place-address.hbs +1 -0
  7. package/addon/components/display-place.hbs +45 -56
  8. package/addon/components/display-place.js +21 -0
  9. package/addon/components/entity/form.hbs +24 -0
  10. package/addon/components/entity/form.js +30 -0
  11. package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
  12. package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
  13. package/addon/components/layout/fleet-ops-sidebar.js +8 -0
  14. package/addon/components/leaflet-draw-control.js +1 -1
  15. package/addon/components/maintenance-schedule/form.js +2 -1
  16. package/addon/components/map/drawer/driver-listing.js +5 -5
  17. package/addon/components/map/drawer/geofence-event-listing.js +5 -2
  18. package/addon/components/map/drawer/place-listing.js +5 -5
  19. package/addon/components/map/drawer/position-listing.js +102 -21
  20. package/addon/components/map/drawer/vehicle-listing.js +5 -5
  21. package/addon/components/map/google-live-map.hbs +9 -0
  22. package/addon/components/map/google-live-map.js +445 -0
  23. package/addon/components/map/leaflet-live-map.hbs +186 -142
  24. package/addon/components/map/leaflet-live-map.js +121 -41
  25. package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
  26. package/addon/components/map/toolbar/zones-panel.hbs +9 -9
  27. package/addon/components/map/toolbar.hbs +3 -3
  28. package/addon/components/map/toolbar.js +9 -1
  29. package/addon/components/modals/map-layer-form.hbs +132 -63
  30. package/addon/components/modals/orchestrator-import.hbs +4 -11
  31. package/addon/components/modals/orchestrator-import.js +17 -1
  32. package/addon/components/modals/update-order-activity.hbs +1 -1
  33. package/addon/components/orchestrator/order-pool.js +3 -2
  34. package/addon/components/orchestrator-workbench.js +29 -41
  35. package/addon/components/order/details/detail.js +2 -2
  36. package/addon/components/order/details/payload.hbs +5 -5
  37. package/addon/components/order/details/payload.js +18 -1
  38. package/addon/components/order/details/route.hbs +2 -2
  39. package/addon/components/order/details/route.js +76 -3
  40. package/addon/components/order/form/payload.hbs +33 -0
  41. package/addon/components/order/form/payload.js +1 -0
  42. package/addon/components/order/form/route.hbs +8 -8
  43. package/addon/components/order/form/route.js +78 -18
  44. package/addon/components/order/form/service-rate.hbs +1 -1
  45. package/addon/components/order/form.js +2 -2
  46. package/addon/components/order/route-editor.hbs +209 -147
  47. package/addon/components/order/route-editor.js +143 -102
  48. package/addon/components/place/address.hbs +1 -0
  49. package/addon/components/place/address.js +12 -0
  50. package/addon/components/route-list.hbs +43 -55
  51. package/addon/components/route-list.js +47 -0
  52. package/addon/components/route-optimization-wizard-panel.js +3 -4
  53. package/addon/components/service-rate/details.hbs +6 -6
  54. package/addon/components/service-rate/form.hbs +28 -15
  55. package/addon/components/vehicle/card.hbs +4 -4
  56. package/addon/controllers/maintenance/schedules/index.js +2 -1
  57. package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
  58. package/addon/controllers/operations/orders/index/details.js +113 -15
  59. package/addon/controllers/operations/orders/index/new.js +1 -1
  60. package/addon/controllers/operations/orders/index.js +8 -1
  61. package/addon/controllers/operations/service-rates/index/edit.js +1 -0
  62. package/addon/controllers/operations/service-rates/index/new.js +1 -0
  63. package/addon/controllers/settings/map.js +97 -0
  64. package/addon/controllers/settings/routing.js +47 -12
  65. package/addon/extension.js +11 -0
  66. package/addon/helpers/f-to-int.js +6 -0
  67. package/addon/helpers/place-address-html.js +6 -0
  68. package/addon/instance-initializers/register-google-routes.js +17 -0
  69. package/addon/instance-initializers/register-osrm.js +5 -0
  70. package/addon/routes/application.js +9 -3
  71. package/addon/routes/operations/orchestrator.js +13 -0
  72. package/addon/routes/operations/orders/index/details/virtual.js +1 -0
  73. package/addon/routes/operations/orders/index/details.js +47 -24
  74. package/addon/routes/operations/orders/index/new.js +9 -0
  75. package/addon/routes/operations/orders/index.js +0 -1
  76. package/addon/routes/settings/map.js +3 -0
  77. package/addon/routes.js +1 -0
  78. package/addon/services/entity-actions.js +1 -1
  79. package/addon/services/equipment-actions.js +4 -6
  80. package/addon/services/geofence-event-bus.js +2 -1
  81. package/addon/services/geofence.js +204 -168
  82. package/addon/services/google-routes.js +300 -0
  83. package/addon/services/leaflet-draw-restriction.js +2 -1
  84. package/addon/services/leaflet-map-manager.js +21 -0
  85. package/addon/services/maintenance-actions.js +4 -6
  86. package/addon/services/map-adapter/google.js +2205 -0
  87. package/addon/services/map-adapter/leaflet.js +1042 -0
  88. package/addon/services/map-adapter-interface.js +551 -0
  89. package/addon/services/map-manager.js +768 -0
  90. package/addon/services/map-settings.js +75 -0
  91. package/addon/services/movement-tracker.js +83 -32
  92. package/addon/services/order-actions.js +11 -1
  93. package/addon/services/osrm.js +79 -6
  94. package/addon/services/part-actions.js +4 -6
  95. package/addon/services/position-playback.js +135 -62
  96. package/addon/services/route-engine.js +83 -0
  97. package/addon/services/service-areas.js +8 -1
  98. package/addon/services/service-rate-actions.js +8 -3
  99. package/addon/styles/fleetops-engine.css +555 -0
  100. package/addon/templates/management/vehicles/index.hbs +0 -1
  101. package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
  102. package/addon/templates/operations/orders/index/details.hbs +0 -1
  103. package/addon/templates/settings/map.hbs +53 -0
  104. package/addon/templates/settings/routing.hbs +15 -4
  105. package/addon/utils/fleet-ops-options.js +34 -0
  106. package/addon/utils/has-valid-coordinates.js +6 -0
  107. package/addon/utils/map-provider-options.js +6 -0
  108. package/addon/utils/order-route-editing.js +86 -0
  109. package/addon/utils/order-route-summary.js +20 -0
  110. package/addon/utils/place-address-html.js +41 -0
  111. package/addon/utils/route-colors.js +43 -12
  112. package/addon/utils/route-visualization.js +93 -0
  113. package/addon/utils/serialize-payload.js +1 -1
  114. package/addon/utils/vendor-integration.js +2 -1
  115. package/app/components/admin/map-settings.js +1 -0
  116. package/app/components/admin/routing-settings.js +1 -0
  117. package/app/components/cell/order-route-type.js +1 -0
  118. package/app/components/cell/place-address.js +1 -0
  119. package/app/components/map/google-live-map.js +1 -0
  120. package/app/components/place/address.js +1 -0
  121. package/app/controllers/operations/orders/index/details/virtual.js +1 -0
  122. package/app/controllers/settings/map.js +1 -0
  123. package/app/helpers/f-to-int.js +1 -0
  124. package/app/helpers/place-address-html.js +1 -0
  125. package/app/routes/settings/map.js +1 -0
  126. package/app/services/map-adapter/google.js +1 -0
  127. package/app/services/map-adapter/leaflet.js +1 -0
  128. package/app/services/map-adapter-interface.js +1 -0
  129. package/app/services/map-manager.js +1 -0
  130. package/app/services/map-settings.js +1 -0
  131. package/app/templates/settings/map.js +1 -0
  132. package/app/utils/has-valid-coordinates.js +1 -0
  133. package/app/utils/order-route-editing.js +2 -0
  134. package/app/utils/order-route-summary.js +1 -0
  135. package/app/utils/place-address-html.js +1 -0
  136. package/app/utils/route-colors.js +1 -1
  137. package/app/utils/route-visualization.js +1 -0
  138. package/assets/images/drawing-controls-spritesheet.svg +156 -0
  139. package/composer.json +1 -1
  140. package/extension.json +1 -1
  141. package/package.json +3 -3
  142. package/server/src/Auth/Schemas/FleetOps.php +5 -0
  143. package/server/src/Http/Controllers/Api/v1/OrderController.php +36 -8
  144. package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
  145. package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
  146. package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
  147. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +7 -2
  148. package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
  149. package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
  150. package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
  151. package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
  152. package/server/src/Http/Controllers/Internal/v1/OrderController.php +61 -34
  153. package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
  154. package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
  155. package/server/src/Http/Filter/OrderFilter.php +2 -1
  156. package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
  157. package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
  158. package/server/src/Http/Resources/v1/Order.php +20 -2
  159. package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
  160. package/server/src/Models/Entity.php +7 -0
  161. package/server/src/Models/Order.php +81 -8
  162. package/server/src/Models/OrderConfig.php +15 -0
  163. package/server/src/Models/Payload.php +131 -27
  164. package/server/src/Models/Place.php +182 -48
  165. package/server/src/Models/ServiceRate.php +125 -32
  166. package/server/src/Observers/PurchaseRateObserver.php +3 -5
  167. package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
  168. package/server/src/Rules/ComputableAlgo.php +1 -3
  169. package/server/src/Support/Algo.php +87 -1
  170. package/server/src/Support/Utils.php +69 -0
  171. package/server/src/routes.php +4 -0
  172. package/translations/en-us.yaml +1937 -1916
@@ -184,9 +184,17 @@ class SettingController extends Controller
184
184
  */
185
185
  public function saveRoutingSettings(Request $request)
186
186
  {
187
- $router = $request->input('router');
188
- $unit = $request->input('unit', 'km');
189
- Setting::configureCompany('routing', ['router' => $router, 'unit' => $unit]);
187
+ $displayEngine = $request->input('display_engine', $request->input('router', 'osrm'));
188
+ $optimizationEngine = $request->input('optimization_engine', $displayEngine);
189
+ $unit = $request->input('unit', 'km');
190
+ Setting::configureCompany('routing', [
191
+ 'router' => $displayEngine,
192
+ 'display_engine' => $displayEngine,
193
+ 'optimization_engine' => $optimizationEngine,
194
+ 'routing_display_engine' => $displayEngine,
195
+ 'routing_optimization_engine' => $optimizationEngine,
196
+ 'unit' => $unit,
197
+ ]);
190
198
 
191
199
  return response()->json([
192
200
  'status' => 'ok',
@@ -203,6 +211,14 @@ class SettingController extends Controller
203
211
  {
204
212
  $routingSettings = Setting::lookupCompany('routing', ['router' => 'osrm', 'unit' => 'km']);
205
213
 
214
+ $displayEngine = data_get($routingSettings, 'display_engine', data_get($routingSettings, 'routing_display_engine', data_get($routingSettings, 'router', 'osrm')));
215
+ $optimizationEngine = data_get($routingSettings, 'optimization_engine', data_get($routingSettings, 'routing_optimization_engine', $displayEngine));
216
+ $routingSettings['router'] = $displayEngine;
217
+ $routingSettings['display_engine'] = $displayEngine;
218
+ $routingSettings['optimization_engine'] = $optimizationEngine;
219
+ $routingSettings['routing_display_engine'] = $displayEngine;
220
+ $routingSettings['routing_optimization_engine'] = $optimizationEngine;
221
+
206
222
  // always default to km if no unit is set
207
223
  if (!isset($routingSettings['unit'])) {
208
224
  $routingSettings['unit'] = 'km';
@@ -211,6 +227,93 @@ class SettingController extends Controller
211
227
  return response()->json($routingSettings);
212
228
  }
213
229
 
230
+ /**
231
+ * Retrieve and return the map provider settings for the current company.
232
+ *
233
+ * The Google Maps API key is sourced exclusively from the system-level
234
+ * services configuration managed by the core-api admin settings panel
235
+ * (services.google_maps.api_key). FleetOps never stores or manages the
236
+ * key independently — it simply reads it from the shared system config
237
+ * and passes it to the frontend so the Google Maps adapter can initialise.
238
+ *
239
+ * @return \Illuminate\Http\JsonResponse
240
+ */
241
+ public function getMapSettings()
242
+ {
243
+ $defaults = [
244
+ 'mapProvider' => 'leaflet',
245
+ ];
246
+
247
+ $systemMapSettings = Setting::lookup('fleet-ops.map-settings', []);
248
+ $mapSettings = Setting::lookupFromCompany('fleet-ops.map-settings', $defaults);
249
+ $mapSettings['mapProvider'] = data_get($mapSettings, 'mapProvider') ?: data_get($systemMapSettings, 'mapProvider', 'leaflet');
250
+
251
+ // Source the Google Maps API key from the system-level services config
252
+ // that is managed by the core-api admin settings panel. This ensures a
253
+ // single source of truth and avoids duplicating key management.
254
+ $mapSettings['googleMapsApiKey'] = config('services.google_maps.api_key', env('GOOGLE_MAPS_API_KEY', ''));
255
+ $mapSettings['googleMapsMapId'] = data_get($systemMapSettings, 'googleMapsMapId', '');
256
+
257
+ return response()->json($mapSettings);
258
+ }
259
+
260
+ /**
261
+ * Persist the map provider settings for the current company.
262
+ *
263
+ * The Google Maps API key is managed entirely through the core-api admin
264
+ * settings panel (Settings → Services → Google Maps) and is therefore
265
+ * never accepted or stored by this endpoint. Only the provider selection
266
+ * and display preferences are persisted here.
267
+ *
268
+ * @return \Illuminate\Http\JsonResponse
269
+ */
270
+ public function saveMapSettings(Request $request)
271
+ {
272
+ $settings = $request->input('settings', []);
273
+
274
+ // The API key is managed at the system level via core-api — strip it
275
+ // from the payload in case a client accidentally sends it.
276
+ unset($settings['googleMapsApiKey']);
277
+
278
+ // Validate provider value
279
+ $allowedProviders = ['leaflet', 'google'];
280
+ if (isset($settings['mapProvider']) && !in_array($settings['mapProvider'], $allowedProviders)) {
281
+ $settings['mapProvider'] = 'leaflet';
282
+ }
283
+
284
+ Setting::configureCompany('fleet-ops.map-settings', $settings);
285
+
286
+ return response()->json($this->getMapSettings()->getData(true));
287
+ }
288
+
289
+ public function getAdminMapSettings()
290
+ {
291
+ $defaults = [
292
+ 'mapProvider' => 'leaflet',
293
+ 'googleMapsMapId' => '',
294
+ ];
295
+
296
+ return response()->json(Setting::lookup('fleet-ops.map-settings', $defaults));
297
+ }
298
+
299
+ public function saveAdminMapSettings(Request $request)
300
+ {
301
+ $allowedProviders = ['leaflet', 'google'];
302
+ $mapProvider = $request->input('mapProvider', 'leaflet');
303
+ if (!in_array($mapProvider, $allowedProviders)) {
304
+ $mapProvider = 'leaflet';
305
+ }
306
+
307
+ $settings = [
308
+ 'mapProvider' => $mapProvider,
309
+ 'googleMapsMapId' => (string) $request->input('googleMapsMapId', ''),
310
+ ];
311
+
312
+ Setting::configure('fleet-ops.map-settings', $settings);
313
+
314
+ return response()->json($this->getAdminMapSettings()->getData(true));
315
+ }
316
+
214
317
  /**
215
318
  * Retrieve driver scheduling settings for the current company.
216
319
  *
@@ -44,10 +44,11 @@ class OrderFilter extends Filter
44
44
  // eager load main relationships to reduce N+1 overhead
45
45
  $this->builder->with([
46
46
  'payload.entities',
47
- 'payload.waypoints',
48
47
  'payload.pickup',
49
48
  'payload.dropoff',
50
49
  'payload.return',
50
+ 'payload.firstWaypointMarker.place',
51
+ 'payload.lastWaypointMarker.place',
51
52
  'trackingNumber',
52
53
  'trackingStatuses',
53
54
  'driverAssigned' => function ($query) {
@@ -31,10 +31,10 @@ class CreateServiceRateRequest extends FleetbaseRequest
31
31
  'service_type' => [Rule::requiredIf($this->isMethod('POST')), 'string'],
32
32
  'service_area' => [Rule::exists('service_areas', 'public_id')->whereNull('deleted_at')],
33
33
  'zone' => [Rule::exists('zones', 'public_id')->whereNull('deleted_at')],
34
- 'rate_calculation_method' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'in:fixed_meter,fixed_rate,per_meter,per_drop,algo'],
34
+ 'rate_calculation_method' => [Rule::requiredIf($this->isMethod('POST')), 'string', 'in:fixed_meter,fixed_rate,per_meter,per_drop,algo,parcel'],
35
35
  'currency' => ['required', 'size:3'],
36
36
  'base_fee' => ['numeric'],
37
- 'per_meter_unit' => ['required_if:rate_calculation_method,per_meter', 'string', 'in:km,m'],
37
+ 'per_meter_unit' => ['required_if:rate_calculation_method,per_meter', 'string', 'in:km,m,ft,yd,mi'],
38
38
  'per_meter_flat_rate_fee' => ['required_if:rate_calculation_method,per_meter', 'numeric'],
39
39
  'meter_fees' => [Rule::requiredIf(function () {
40
40
  return in_array($this->input('rate_calculation_method'), ['fixed_meter', 'fixed_rate']);
@@ -19,6 +19,8 @@ class Payload extends FleetbaseResource
19
19
  public function toArray($request): array
20
20
  {
21
21
  $isInternal = Http::isInternalRequest();
22
+ $pickup = $this->index_pickup_place;
23
+ $dropoff = $this->index_dropoff_place;
22
24
 
23
25
  return [
24
26
  'id' => $this->when($isInternal, $this->id, $this->public_id),
@@ -30,24 +32,24 @@ class Payload extends FleetbaseResource
30
32
  'return_uuid' => $this->when($isInternal, $this->return_uuid),
31
33
 
32
34
  // Minimal pickup - only what's displayed in the table
33
- 'pickup' => $this->whenLoaded('pickup', function () {
34
- return new Place($this->pickup);
35
+ 'pickup' => $this->when($pickup, function () use ($pickup) {
36
+ return new Place($pickup);
35
37
  }),
36
38
 
37
39
  // Minimal dropoff - only what's displayed in the table
38
- 'dropoff' => $this->whenLoaded('dropoff', function () {
39
- return new Place($this->dropoff);
40
+ 'dropoff' => $this->when($dropoff, function () use ($dropoff) {
41
+ return new Place($dropoff);
40
42
  }),
41
43
 
42
44
  // Entity count instead of full entities
43
45
  'entities_count' => $this->whenLoaded('entities', function () {
44
46
  return $this->entities->count();
45
- }),
47
+ }, $this->entities()->count()),
46
48
 
47
- // Waypoint count instead of full waypoints
49
+ // Waypoint count for lightweight route-shape summaries.
48
50
  'waypoints_count' => $this->whenLoaded('waypoints', function () {
49
51
  return $this->waypoints->count();
50
- }),
52
+ }, $this->waypoints()->count()),
51
53
 
52
54
  'type' => $this->type,
53
55
  'created_at' => $this->created_at,
@@ -2,6 +2,7 @@
2
2
 
3
3
  namespace Fleetbase\FleetOps\Http\Resources\v1;
4
4
 
5
+ use Fleetbase\FleetOps\Models\Contact as ContactModel;
5
6
  use Fleetbase\FleetOps\Support\Utils;
6
7
  use Fleetbase\Http\Resources\Comment;
7
8
  use Fleetbase\Http\Resources\File;
@@ -45,7 +46,6 @@ class Order extends FleetbaseResource
45
46
  'tracking_number_uuid' => $this->when($isInternal, $this->tracking_number_uuid),
46
47
  'driver_assigned_uuid' => $this->when($isInternal, $this->driver_assigned_uuid),
47
48
  'vehicle_assigned_uuid'=> $this->when($isInternal, $this->vehicle_assigned_uuid),
48
- 'service_quote_uuid' => $this->when($isInternal, $this->service_quote_uuid),
49
49
  'has_driver_assigned' => $this->when($isInternal, $this->has_driver_assigned),
50
50
  'is_scheduled' => $this->when($isInternal, $this->is_scheduled),
51
51
  'order_config_uuid' => $this->when($isInternal, $this->order_config_uuid),
@@ -57,7 +57,7 @@ class Order extends FleetbaseResource
57
57
  $orderConfigPublicId
58
58
  ),
59
59
  'customer' => $this->whenLoaded('customer', function () {
60
- return $this->setCustomerType($this->transformMorphResource($this->customer));
60
+ return $this->setCustomerType($this->transformOrderCustomerResource($this->customer));
61
61
  }),
62
62
  'payload' => new Payload($this->payload),
63
63
  'facilitator' => $this->whenLoaded('facilitator', function () {
@@ -170,6 +170,24 @@ class Order extends FleetbaseResource
170
170
  return (new \Illuminate\Http\Resources\Json\JsonResource($model))->resolve();
171
171
  }
172
172
 
173
+ /**
174
+ * Transform the order customer resource without nesting the customer's user relationship.
175
+ *
176
+ * @param \Illuminate\Database\Eloquent\Model|null $model
177
+ *
178
+ * @return array|null
179
+ */
180
+ protected function transformOrderCustomerResource($model)
181
+ {
182
+ $resolved = $this->transformMorphResource($model);
183
+
184
+ if ($model instanceof ContactModel && is_array($resolved)) {
185
+ unset($resolved['user']);
186
+ }
187
+
188
+ return $resolved;
189
+ }
190
+
173
191
  /**
174
192
  * Transform the resource into an webhook payload.
175
193
  *
@@ -23,8 +23,8 @@ class ServiceQuote extends FleetbaseResource
23
23
  'public_id' => $this->when(Http::isInternalRequest(), $this->public_id),
24
24
  'service_rate_uuid' => $this->when(Http::isInternalRequest(), $this->service_rate_uuid),
25
25
  'payload_uuid' => $this->when(Http::isInternalRequest(), $this->payload_uuid),
26
- 'service_rate_name' => data_get($this, 'serviceRate.name'),
27
- 'service_name' => data_get($this, 'serviceRate.name'),
26
+ 'service_rate_name' => data_get($this, 'serviceRate.service_name'),
27
+ 'service_name' => data_get($this, 'serviceRate.service_name'),
28
28
  'service_rate' => $this->when(Http::isPublicRequest(), data_get($this, 'serviceRate.public_id')),
29
29
  'facilitator' => $this->when(Http::isPublicRequest(), data_get($this, 'integratedVendor.public_id')),
30
30
  'items' => ServiceQuoteItem::collection($this->items),
@@ -168,6 +168,13 @@ class Entity extends Model
168
168
  ])->render();
169
169
  }
170
170
 
171
+ public function setTypeAttribute(?string $type = null): void
172
+ {
173
+ $type = is_string($type) ? trim($type) : $type;
174
+
175
+ $this->attributes['type'] = filled($type) ? Str::snake($type) : null;
176
+ }
177
+
171
178
  /**
172
179
  * @var BelongsTo
173
180
  */
@@ -1055,9 +1055,7 @@ class Order extends Model
1055
1055
  'meta' => $meta,
1056
1056
  ]);
1057
1057
 
1058
- $this->purchase_rate_uuid = $purchasedRate->uuid;
1059
-
1060
- return $this->save();
1058
+ return $this->attachPurchaseRate($purchasedRate);
1061
1059
  }
1062
1060
 
1063
1061
  /**
@@ -1096,14 +1094,48 @@ class Order extends Model
1096
1094
  'meta' => $meta,
1097
1095
  ]);
1098
1096
 
1099
- return $this->updateQuietly([
1100
- 'purchase_rate_uuid' => $purchasedRate->uuid,
1101
- ]);
1097
+ return $this->attachPurchaseRate($purchasedRate);
1102
1098
  }
1103
1099
 
1104
1100
  return false;
1105
1101
  }
1106
1102
 
1103
+ /**
1104
+ * Attach a purchase rate to the order and void any superseded transaction.
1105
+ */
1106
+ public function attachPurchaseRate(PurchaseRate $purchaseRate): bool
1107
+ {
1108
+ $oldTransaction = $this->transaction_uuid
1109
+ ? Transaction::where('uuid', $this->transaction_uuid)->first()
1110
+ : null;
1111
+
1112
+ $attached = (bool) $this->updateQuietly([
1113
+ 'purchase_rate_uuid' => $purchaseRate->uuid,
1114
+ 'transaction_uuid' => $purchaseRate->transaction_uuid,
1115
+ ]);
1116
+
1117
+ if ($attached) {
1118
+ $this->setRelation('purchaseRate', $purchaseRate);
1119
+
1120
+ if ($purchaseRate->transaction_uuid) {
1121
+ $this->setRelation('transaction', Transaction::where('uuid', $purchaseRate->transaction_uuid)->first());
1122
+ }
1123
+
1124
+ if (
1125
+ $oldTransaction instanceof Transaction
1126
+ && $oldTransaction->uuid !== $purchaseRate->transaction_uuid
1127
+ && !$oldTransaction->trashed()
1128
+ ) {
1129
+ $oldTransaction->update([
1130
+ 'status' => Transaction::STATUS_VOIDED,
1131
+ 'voided_at' => now(),
1132
+ ]);
1133
+ }
1134
+ }
1135
+
1136
+ return $attached;
1137
+ }
1138
+
1107
1139
  /**
1108
1140
  * Creates a transaction for the order without a service quote.
1109
1141
  * This method is used when an order is made without selecting a specific service quote.
@@ -1122,7 +1154,7 @@ class Order extends Model
1122
1154
  'gateway_transaction_id' => Transaction::generateNumber(),
1123
1155
  'gateway' => 'internal',
1124
1156
  'amount' => 0,
1125
- 'currency' => data_get($this->company, 'country') ? Utils::getCurrenyFromCountryCode(data_get($this->company, 'country')) : 'SGD',
1157
+ 'currency' => Utils::getCompanyTransactionCurrency($this->company ?? $this->company_uuid),
1126
1158
  'description' => 'Dispatch order',
1127
1159
  'type' => 'dispatch',
1128
1160
  'status' => 'success',
@@ -1700,6 +1732,16 @@ class Order extends Model
1700
1732
 
1701
1733
  if (Str::isUuid($this->order_config_uuid)) {
1702
1734
  $orderConfig = OrderConfig::where('uuid', $this->order_config_uuid)->withTrashed()->first();
1735
+ if ($orderConfig instanceof OrderConfig) {
1736
+ $orderConfig->setOrderContext($this);
1737
+
1738
+ return $orderConfig;
1739
+ }
1740
+ }
1741
+
1742
+ $company = $this->relationLoaded('company') ? $this->company : $this->company()->first();
1743
+ $orderConfig = OrderConfig::defaultOrCreate($company);
1744
+ if ($orderConfig instanceof OrderConfig) {
1703
1745
  $orderConfig->setOrderContext($this);
1704
1746
 
1705
1747
  return $orderConfig;
@@ -1708,6 +1750,37 @@ class Order extends Model
1708
1750
  return null;
1709
1751
  }
1710
1752
 
1753
+ /**
1754
+ * Ensure the order has a persisted order config and normalized type.
1755
+ */
1756
+ public function ensureOrderConfig(): ?OrderConfig
1757
+ {
1758
+ $orderConfig = $this->config();
1759
+
1760
+ if (!$orderConfig) {
1761
+ return null;
1762
+ }
1763
+
1764
+ $updates = [];
1765
+
1766
+ if ($this->order_config_uuid !== $orderConfig->uuid) {
1767
+ $updates['order_config_uuid'] = $orderConfig->uuid;
1768
+ }
1769
+
1770
+ if (empty($this->type) || $this->type === 'default' || $this->type !== $orderConfig->key) {
1771
+ $updates['type'] = $orderConfig->key;
1772
+ }
1773
+
1774
+ if (!empty($updates)) {
1775
+ $this->fill($updates);
1776
+ $this->save();
1777
+ }
1778
+
1779
+ $this->setRelation('orderConfig', $orderConfig);
1780
+
1781
+ return $orderConfig;
1782
+ }
1783
+
1711
1784
  /**
1712
1785
  * Retrieves the flow configuration from the order config.
1713
1786
  *
@@ -1721,7 +1794,7 @@ class Order extends Model
1721
1794
  public function getConfigFlow(): array
1722
1795
  {
1723
1796
  $orderConfig = $this->config();
1724
- if (is_array($orderConfig->flow)) {
1797
+ if ($orderConfig && is_array($orderConfig->flow)) {
1725
1798
  return $orderConfig->flow;
1726
1799
  }
1727
1800
 
@@ -5,6 +5,7 @@ namespace Fleetbase\FleetOps\Models;
5
5
  use Fleetbase\Casts\Json;
6
6
  use Fleetbase\FleetOps\Casts\OrderConfigEntities;
7
7
  use Fleetbase\FleetOps\Flow\Activity;
8
+ use Fleetbase\FleetOps\Support\FleetOps;
8
9
  use Fleetbase\Models\Company;
9
10
  use Fleetbase\Models\Model;
10
11
  use Fleetbase\Support\Auth;
@@ -498,4 +499,18 @@ class OrderConfig extends Model
498
499
  {
499
500
  return static::where(['namespace' => 'system:order-config:transport', 'company_uuid' => $company ? $company->uuid : session('company')])->first();
500
501
  }
502
+
503
+ /**
504
+ * Get the default transport config, creating it for the company if needed.
505
+ */
506
+ public static function defaultOrCreate(?Company $company = null): ?self
507
+ {
508
+ $company ??= Company::where('uuid', session('company'))->first();
509
+
510
+ if (!$company) {
511
+ return static::default($company);
512
+ }
513
+
514
+ return static::default($company) ?? FleetOps::createTransportConfig($company);
515
+ }
501
516
  }
@@ -137,6 +137,26 @@ class Payload extends Model
137
137
  return $this->hasMany(Waypoint::class)->with(['place']);
138
138
  }
139
139
 
140
+ /**
141
+ * The first waypoint marker for lightweight index fallback.
142
+ */
143
+ public function firstWaypointMarker()
144
+ {
145
+ return $this->hasOne(Waypoint::class, 'payload_uuid', 'uuid')
146
+ ->oldestOfMany('order')
147
+ ->with(['place']);
148
+ }
149
+
150
+ /**
151
+ * The last waypoint marker for lightweight index fallback.
152
+ */
153
+ public function lastWaypointMarker()
154
+ {
155
+ return $this->hasOne(Waypoint::class, 'payload_uuid', 'uuid')
156
+ ->latestOfMany('order')
157
+ ->with(['place']);
158
+ }
159
+
140
160
  public function getTotalEntitiesAttribute()
141
161
  {
142
162
  return $this->entities()->count();
@@ -534,48 +554,104 @@ class Payload extends Model
534
554
  return $this;
535
555
  }
536
556
 
537
- $placeIds = [];
557
+ $this->loadMissing('waypointMarkers');
558
+ $keptWaypointIds = [];
559
+ $availableWaypointMarkers = $this->waypointMarkers()->get();
538
560
 
539
- // collect all place ids to insert
540
561
  foreach ($waypoints as $index => $attributes) {
562
+ $raw = $attributes;
563
+ $type = data_get($raw, 'type', 'dropoff');
564
+ $customerUuidIn = data_get($raw, 'customer_uuid');
565
+ $customerPubIdIn = data_get($raw, 'customer_id');
566
+ $customerType = data_get($raw, 'customer_type', 'fleetops:contact');
567
+
541
568
  if (Utils::isset($attributes, 'place') && is_array(Utils::get($attributes, 'place'))) {
542
569
  $attributes = Utils::get($attributes, 'place');
543
570
  }
544
571
 
545
- if (is_array($attributes) && array_key_exists('place_uuid', $attributes)) {
546
- $placeIds[] = $attributes['place_uuid'];
572
+ $placeUuid = null;
573
+
574
+ if (
575
+ is_array($attributes)
576
+ && isset($attributes['place_uuid'])
577
+ && Place::where('uuid', $attributes['place_uuid'])->exists()
578
+ ) {
579
+ $placeUuid = $attributes['place_uuid'];
580
+ } elseif (
581
+ is_array($attributes)
582
+ && isset($attributes['uuid'])
583
+ && ($resolvedUuid = Place::where('uuid', $attributes['uuid'])->value('uuid'))
584
+ ) {
585
+ $placeUuid = $resolvedUuid;
586
+ } elseif (
587
+ is_array($attributes)
588
+ && isset($attributes['id'])
589
+ && ($resolvedUuid = Place::where('public_id', $attributes['id'])->value('uuid'))
590
+ ) {
591
+ $placeUuid = $resolvedUuid;
547
592
  } else {
548
- $placeUuid = Place::insertFromMixed($attributes);
549
- $placeIds[] = $placeUuid;
593
+ $place = Place::createFromMixed($attributes);
594
+
595
+ if ($place instanceof Place && isset($attributes['uuid']) && $place->uuid !== $attributes['uuid']) {
596
+ $place->updateMeta('search_uuid', $attributes['uuid']);
597
+ }
598
+
599
+ $placeUuid = $place->uuid;
550
600
  }
551
- }
552
601
 
553
- /** @return \Illuminate\Database\Eloquent\Collection $waypointMakers */
554
- $waypointMakers = $this->waypointMarkers()->get();
602
+ $customerUuid = null;
603
+ $customerTypeNamespace = null;
555
604
 
556
- // remove all waypoints that are not included in the placeids
557
- $waypointMakers = $waypointMakers->filter(function ($waypointMarker) use ($placeIds) {
558
- if (!in_array($waypointMarker->place_uuid, $placeIds)) {
559
- $waypointMarker->delete();
605
+ if ($customerType) {
606
+ $customerTypeNamespace = Utils::getMutationType($customerType);
607
+ $customerModel = app($customerTypeNamespace);
608
+
609
+ if ($customerUuidIn && $customerModel->where('uuid', $customerUuidIn)->exists()) {
610
+ $customerUuid = $customerUuidIn;
611
+ }
612
+
613
+ if (!$customerUuid && $customerPubIdIn) {
614
+ $maybeUuid = $customerModel->where('public_id', $customerPubIdIn)->value('uuid');
615
+ if ($maybeUuid) {
616
+ $customerUuid = $maybeUuid;
617
+ } else {
618
+ $customerTypeNamespace = null;
619
+ }
620
+ }
621
+
622
+ if (!$customerUuid) {
623
+ $customerTypeNamespace = null;
624
+ }
560
625
  }
561
626
 
562
- return in_array($waypointMarker->place_uuid, $placeIds);
563
- });
627
+ $values = [
628
+ 'payload_uuid' => $this->uuid,
629
+ 'place_uuid' => $placeUuid,
630
+ 'order' => $index,
631
+ 'type' => $type,
632
+ 'customer_uuid' => $customerUuid,
633
+ 'customer_type' => $customerTypeNamespace,
634
+ ];
635
+
636
+ $existingWaypointMarker = $availableWaypointMarkers
637
+ ->first(fn ($waypointMarker) => $waypointMarker->place_uuid === $placeUuid && !in_array($waypointMarker->uuid, $keptWaypointIds));
638
+
639
+ if ($existingWaypointMarker) {
640
+ $existingWaypointMarker->update($values);
641
+ $keptWaypointIds[] = $existingWaypointMarker->uuid;
642
+ continue;
643
+ }
564
644
 
565
- // update or create waypoint markers
566
- foreach ($placeIds as $placeId) {
567
- Waypoint::updateOrCreate(
568
- [
569
- 'payload_uuid' => $this->uuid,
570
- 'place_uuid' => $placeId,
571
- ],
572
- [
573
- 'payload_uuid' => $this->uuid,
574
- 'place_uuid' => $placeId,
575
- ]
576
- );
645
+ $waypointRecord = Waypoint::create($values);
646
+ $keptWaypointIds[] = $waypointRecord->uuid;
577
647
  }
578
648
 
649
+ $this->waypointMarkers()
650
+ ->whereNotIn('uuid', $keptWaypointIds)
651
+ ->get()
652
+ ->each
653
+ ->delete();
654
+
579
655
  return $this->refresh()->load(['waypoints']);
580
656
  }
581
657
 
@@ -673,6 +749,34 @@ class Payload extends Model
673
749
  return null;
674
750
  }
675
751
 
752
+ /**
753
+ * Get the lightweight pickup fallback for index resources.
754
+ */
755
+ public function getIndexPickupPlaceAttribute(): ?Place
756
+ {
757
+ if ($this->pickup instanceof Place) {
758
+ return $this->pickup;
759
+ }
760
+
761
+ $this->loadMissing('firstWaypointMarker.place');
762
+
763
+ return data_get($this, 'firstWaypointMarker.place');
764
+ }
765
+
766
+ /**
767
+ * Get the lightweight dropoff fallback for index resources.
768
+ */
769
+ public function getIndexDropoffPlaceAttribute(): ?Place
770
+ {
771
+ if ($this->dropoff instanceof Place) {
772
+ return $this->dropoff;
773
+ }
774
+
775
+ $this->loadMissing('lastWaypointMarker.place');
776
+
777
+ return data_get($this, 'lastWaypointMarker.place');
778
+ }
779
+
676
780
  public function getPickupRegion(): string
677
781
  {
678
782
  $pickup = $this->getPickupOrCurrentWaypoint();