@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.
- package/addon/components/admin/map-settings.hbs +26 -0
- package/addon/components/admin/map-settings.js +46 -0
- package/addon/components/admin/routing-settings.hbs +11 -0
- package/addon/components/cell/order-route-type.hbs +46 -0
- package/addon/components/cell/order-route-type.js +90 -0
- package/addon/components/cell/place-address.hbs +1 -0
- package/addon/components/display-place.hbs +45 -56
- package/addon/components/display-place.js +21 -0
- package/addon/components/entity/form.hbs +24 -0
- package/addon/components/entity/form.js +30 -0
- package/addon/components/layout/fleet-ops-sidebar/driver-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar/fleet-listing.js +5 -5
- package/addon/components/layout/fleet-ops-sidebar.js +8 -0
- package/addon/components/leaflet-draw-control.js +1 -1
- package/addon/components/maintenance-schedule/form.js +2 -1
- package/addon/components/map/drawer/driver-listing.js +5 -5
- package/addon/components/map/drawer/geofence-event-listing.js +5 -2
- package/addon/components/map/drawer/place-listing.js +5 -5
- package/addon/components/map/drawer/position-listing.js +102 -21
- package/addon/components/map/drawer/vehicle-listing.js +5 -5
- package/addon/components/map/google-live-map.hbs +9 -0
- package/addon/components/map/google-live-map.js +445 -0
- package/addon/components/map/leaflet-live-map.hbs +186 -142
- package/addon/components/map/leaflet-live-map.js +121 -41
- package/addon/components/map/toolbar/visibility-control-panel.js +2 -2
- package/addon/components/map/toolbar/zones-panel.hbs +9 -9
- package/addon/components/map/toolbar.hbs +3 -3
- package/addon/components/map/toolbar.js +9 -1
- package/addon/components/modals/map-layer-form.hbs +132 -63
- package/addon/components/modals/orchestrator-import.hbs +4 -11
- package/addon/components/modals/orchestrator-import.js +17 -1
- package/addon/components/modals/update-order-activity.hbs +1 -1
- package/addon/components/orchestrator/order-pool.js +3 -2
- package/addon/components/orchestrator-workbench.js +29 -41
- package/addon/components/order/details/detail.js +2 -2
- package/addon/components/order/details/payload.hbs +5 -5
- package/addon/components/order/details/payload.js +18 -1
- package/addon/components/order/details/route.hbs +2 -2
- package/addon/components/order/details/route.js +76 -3
- package/addon/components/order/form/payload.hbs +33 -0
- package/addon/components/order/form/payload.js +1 -0
- package/addon/components/order/form/route.hbs +8 -8
- package/addon/components/order/form/route.js +78 -18
- package/addon/components/order/form/service-rate.hbs +1 -1
- package/addon/components/order/form.js +2 -2
- package/addon/components/order/route-editor.hbs +209 -147
- package/addon/components/order/route-editor.js +143 -102
- package/addon/components/place/address.hbs +1 -0
- package/addon/components/place/address.js +12 -0
- package/addon/components/route-list.hbs +43 -55
- package/addon/components/route-list.js +47 -0
- package/addon/components/route-optimization-wizard-panel.js +3 -4
- package/addon/components/service-rate/details.hbs +6 -6
- package/addon/components/service-rate/form.hbs +28 -15
- package/addon/components/vehicle/card.hbs +4 -4
- package/addon/controllers/maintenance/schedules/index.js +2 -1
- package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
- package/addon/controllers/operations/orders/index/details.js +113 -15
- package/addon/controllers/operations/orders/index/new.js +1 -1
- package/addon/controllers/operations/orders/index.js +8 -1
- package/addon/controllers/operations/service-rates/index/edit.js +1 -0
- package/addon/controllers/operations/service-rates/index/new.js +1 -0
- package/addon/controllers/settings/map.js +97 -0
- package/addon/controllers/settings/routing.js +47 -12
- package/addon/extension.js +11 -0
- package/addon/helpers/f-to-int.js +6 -0
- package/addon/helpers/place-address-html.js +6 -0
- package/addon/instance-initializers/register-google-routes.js +17 -0
- package/addon/instance-initializers/register-osrm.js +5 -0
- package/addon/routes/application.js +9 -3
- package/addon/routes/operations/orchestrator.js +13 -0
- package/addon/routes/operations/orders/index/details/virtual.js +1 -0
- package/addon/routes/operations/orders/index/details.js +47 -24
- package/addon/routes/operations/orders/index/new.js +9 -0
- package/addon/routes/operations/orders/index.js +0 -1
- package/addon/routes/settings/map.js +3 -0
- package/addon/routes.js +1 -0
- package/addon/services/entity-actions.js +1 -1
- package/addon/services/equipment-actions.js +4 -6
- package/addon/services/geofence-event-bus.js +2 -1
- package/addon/services/geofence.js +204 -168
- package/addon/services/google-routes.js +300 -0
- package/addon/services/leaflet-draw-restriction.js +2 -1
- package/addon/services/leaflet-map-manager.js +21 -0
- package/addon/services/maintenance-actions.js +4 -6
- package/addon/services/map-adapter/google.js +2205 -0
- package/addon/services/map-adapter/leaflet.js +1042 -0
- package/addon/services/map-adapter-interface.js +551 -0
- package/addon/services/map-manager.js +768 -0
- package/addon/services/map-settings.js +75 -0
- package/addon/services/movement-tracker.js +83 -32
- package/addon/services/order-actions.js +11 -1
- package/addon/services/osrm.js +79 -6
- package/addon/services/part-actions.js +4 -6
- package/addon/services/position-playback.js +135 -62
- package/addon/services/route-engine.js +83 -0
- package/addon/services/service-areas.js +8 -1
- package/addon/services/service-rate-actions.js +8 -3
- package/addon/styles/fleetops-engine.css +555 -0
- package/addon/templates/management/vehicles/index.hbs +0 -1
- package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
- package/addon/templates/operations/orders/index/details.hbs +0 -1
- package/addon/templates/settings/map.hbs +53 -0
- package/addon/templates/settings/routing.hbs +15 -4
- package/addon/utils/fleet-ops-options.js +34 -0
- package/addon/utils/has-valid-coordinates.js +6 -0
- package/addon/utils/map-provider-options.js +6 -0
- package/addon/utils/order-route-editing.js +86 -0
- package/addon/utils/order-route-summary.js +20 -0
- package/addon/utils/place-address-html.js +41 -0
- package/addon/utils/route-colors.js +43 -12
- package/addon/utils/route-visualization.js +93 -0
- package/addon/utils/serialize-payload.js +1 -1
- package/addon/utils/vendor-integration.js +2 -1
- package/app/components/admin/map-settings.js +1 -0
- package/app/components/admin/routing-settings.js +1 -0
- package/app/components/cell/order-route-type.js +1 -0
- package/app/components/cell/place-address.js +1 -0
- package/app/components/map/google-live-map.js +1 -0
- package/app/components/place/address.js +1 -0
- package/app/controllers/operations/orders/index/details/virtual.js +1 -0
- package/app/controllers/settings/map.js +1 -0
- package/app/helpers/f-to-int.js +1 -0
- package/app/helpers/place-address-html.js +1 -0
- package/app/routes/settings/map.js +1 -0
- package/app/services/map-adapter/google.js +1 -0
- package/app/services/map-adapter/leaflet.js +1 -0
- package/app/services/map-adapter-interface.js +1 -0
- package/app/services/map-manager.js +1 -0
- package/app/services/map-settings.js +1 -0
- package/app/templates/settings/map.js +1 -0
- package/app/utils/has-valid-coordinates.js +1 -0
- package/app/utils/order-route-editing.js +2 -0
- package/app/utils/order-route-summary.js +1 -0
- package/app/utils/place-address-html.js +1 -0
- package/app/utils/route-colors.js +1 -1
- package/app/utils/route-visualization.js +1 -0
- package/assets/images/drawing-controls-spritesheet.svg +156 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +3 -3
- package/server/src/Auth/Schemas/FleetOps.php +5 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +36 -8
- package/server/src/Http/Controllers/Api/v1/PayloadController.php +14 -1
- package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +2 -0
- package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +7 -2
- package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +2 -0
- package/server/src/Http/Controllers/Internal/v1/LiveController.php +2 -1
- package/server/src/Http/Controllers/Internal/v1/OrchestrationController.php +7 -4
- package/server/src/Http/Controllers/Internal/v1/OrderController.php +61 -34
- package/server/src/Http/Controllers/Internal/v1/ServiceQuoteController.php +3 -2
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +106 -3
- package/server/src/Http/Filter/OrderFilter.php +2 -1
- package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
- package/server/src/Http/Resources/v1/Index/Payload.php +9 -7
- package/server/src/Http/Resources/v1/Order.php +20 -2
- package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
- package/server/src/Models/Entity.php +7 -0
- package/server/src/Models/Order.php +81 -8
- package/server/src/Models/OrderConfig.php +15 -0
- package/server/src/Models/Payload.php +131 -27
- package/server/src/Models/Place.php +182 -48
- package/server/src/Models/ServiceRate.php +125 -32
- package/server/src/Observers/PurchaseRateObserver.php +3 -5
- package/server/src/Orchestration/Engines/VroomOrchestrationEngine.php +59 -23
- package/server/src/Rules/ComputableAlgo.php +1 -3
- package/server/src/Support/Algo.php +87 -1
- package/server/src/Support/Utils.php +69 -0
- package/server/src/routes.php +4 -0
- package/translations/en-us.yaml +1937 -1916
|
@@ -335,24 +335,8 @@ class Place extends Model
|
|
|
335
335
|
|
|
336
336
|
// Before saving or returning this instance check the database for a duplicate address
|
|
337
337
|
// it cannot have any owner, and must belong to this session
|
|
338
|
-
if ($
|
|
339
|
-
$duplicate
|
|
340
|
-
'company_uuid' => $companyUuid,
|
|
341
|
-
'street1' => $instance->street1,
|
|
342
|
-
'city' => $instance->city,
|
|
343
|
-
'country' => $instance->country,
|
|
344
|
-
])
|
|
345
|
-
->when(
|
|
346
|
-
$instance->postal_code !== null,
|
|
347
|
-
fn ($q) => $q->where('postal_code', $instance->postal_code),
|
|
348
|
-
fn ($q) => $q->whereNull('postal_code')
|
|
349
|
-
)
|
|
350
|
-
->whereNull('owner_uuid')
|
|
351
|
-
->first();
|
|
352
|
-
|
|
353
|
-
if ($duplicate) {
|
|
354
|
-
return $duplicate;
|
|
355
|
-
}
|
|
338
|
+
if ($duplicate = static::findExistingSharedPlace($instance->toArray())) {
|
|
339
|
+
return $duplicate;
|
|
356
340
|
}
|
|
357
341
|
|
|
358
342
|
if ($saveInstance) {
|
|
@@ -400,7 +384,7 @@ class Place extends Model
|
|
|
400
384
|
/**
|
|
401
385
|
* Create a new Place instance from a geocoding lookup.
|
|
402
386
|
*
|
|
403
|
-
* @return
|
|
387
|
+
* @return array
|
|
404
388
|
*/
|
|
405
389
|
public static function getValuesFromGeocodingLookup(string $address): array
|
|
406
390
|
{
|
|
@@ -413,6 +397,163 @@ class Place extends Model
|
|
|
413
397
|
return static::getGoogleAddressArray($results->first());
|
|
414
398
|
}
|
|
415
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Compose a fuller geocoding query from structured place attributes.
|
|
402
|
+
*/
|
|
403
|
+
public static function composeGeocodingQuery(array $place): ?string
|
|
404
|
+
{
|
|
405
|
+
$parts = [];
|
|
406
|
+
|
|
407
|
+
foreach (['street1', 'street2', 'city', 'province', 'postal_code', 'country'] as $field) {
|
|
408
|
+
$value = static::normalizePlaceValue(data_get($place, $field));
|
|
409
|
+
if ($value) {
|
|
410
|
+
$parts[] = $value;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (empty($parts)) {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return implode(', ', array_values(array_unique($parts)));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Normalize a place field value for matching and persistence.
|
|
423
|
+
*/
|
|
424
|
+
public static function normalizePlaceValue($value): ?string
|
|
425
|
+
{
|
|
426
|
+
if (!is_scalar($value)) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
$value = trim((string) $value);
|
|
431
|
+
|
|
432
|
+
return $value !== '' ? $value : null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Merge explicit structured place attributes with geocoded fallbacks.
|
|
437
|
+
* Explicit values always win.
|
|
438
|
+
*/
|
|
439
|
+
public static function mergeStructuredPlaceAttributes(array $place, array $geocoded = []): array
|
|
440
|
+
{
|
|
441
|
+
$place = array_merge($geocoded, $place);
|
|
442
|
+
|
|
443
|
+
foreach (['name', 'street1', 'street2', 'city', 'province', 'postal_code', 'country', 'phone'] as $field) {
|
|
444
|
+
$normalized = static::normalizePlaceValue(data_get($place, $field));
|
|
445
|
+
if ($normalized !== null) {
|
|
446
|
+
$place[$field] = $normalized;
|
|
447
|
+
} elseif (array_key_exists($field, $place)) {
|
|
448
|
+
$place[$field] = null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!empty($place['location'])) {
|
|
453
|
+
$place['location'] = Utils::getPointFromMixed($place['location']) ?? $place['location'];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return array_filter($place, fn ($value) => $value !== '');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Find an existing shared place using normalized address identity.
|
|
461
|
+
*/
|
|
462
|
+
public static function findExistingSharedPlace(array $place): ?Place
|
|
463
|
+
{
|
|
464
|
+
if (data_get($place, 'owner_uuid')) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
$companyUuid = data_get($place, 'company_uuid', session('company'));
|
|
469
|
+
$street1 = static::normalizePlaceValue(data_get($place, 'street1'));
|
|
470
|
+
$city = static::normalizePlaceValue(data_get($place, 'city'));
|
|
471
|
+
$country = static::normalizePlaceValue(data_get($place, 'country'));
|
|
472
|
+
|
|
473
|
+
if (!$companyUuid || !$street1 || !$city || !$country) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
$postalCode = static::normalizePlaceValue(data_get($place, 'postal_code'));
|
|
478
|
+
$street2 = static::normalizePlaceValue(data_get($place, 'street2'));
|
|
479
|
+
$location = Utils::getPointFromMixed(data_get($place, 'location'));
|
|
480
|
+
|
|
481
|
+
$existingPlace = static::query()
|
|
482
|
+
->where('company_uuid', $companyUuid)
|
|
483
|
+
->whereNull('owner_uuid')
|
|
484
|
+
->whereNull('deleted_at')
|
|
485
|
+
->where('street1', $street1)
|
|
486
|
+
->where('city', $city)
|
|
487
|
+
->where('country', $country)
|
|
488
|
+
->when(
|
|
489
|
+
$postalCode !== null,
|
|
490
|
+
fn ($query) => $query->where('postal_code', $postalCode)
|
|
491
|
+
)
|
|
492
|
+
->when(
|
|
493
|
+
$postalCode === null,
|
|
494
|
+
fn ($query) => $query->whereNull('postal_code')
|
|
495
|
+
)
|
|
496
|
+
->when(
|
|
497
|
+
$street2 !== null,
|
|
498
|
+
fn ($query) => $query->where('street2', $street2)
|
|
499
|
+
)
|
|
500
|
+
->first();
|
|
501
|
+
|
|
502
|
+
if ($existingPlace) {
|
|
503
|
+
return $existingPlace;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!$location instanceof SpatialPoint) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return static::query()
|
|
511
|
+
->where('company_uuid', $companyUuid)
|
|
512
|
+
->whereNull('owner_uuid')
|
|
513
|
+
->whereNull('deleted_at')
|
|
514
|
+
->where('street1', $street1)
|
|
515
|
+
->where('city', $city)
|
|
516
|
+
->where('country', $country)
|
|
517
|
+
->whereRaw('ST_Equals(location, ST_GeomFromText(?))', [
|
|
518
|
+
sprintf('POINT(%s %s)', $location->getLng(), $location->getLat()),
|
|
519
|
+
])
|
|
520
|
+
->first();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Update safe missing fields on an existing shared place.
|
|
525
|
+
*/
|
|
526
|
+
public static function hydrateSharedPlace(Place $existingPlace, array $values): Place
|
|
527
|
+
{
|
|
528
|
+
$updates = [];
|
|
529
|
+
|
|
530
|
+
foreach (['name', 'street2', 'province', 'postal_code', 'neighborhood', 'district', 'building', 'phone'] as $field) {
|
|
531
|
+
$current = static::normalizePlaceValue($existingPlace->{$field});
|
|
532
|
+
$next = static::normalizePlaceValue(data_get($values, $field));
|
|
533
|
+
|
|
534
|
+
if ($current === null && $next !== null) {
|
|
535
|
+
$updates[$field] = $next;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
$existingLocation = Utils::getPointFromMixed($existingPlace->location);
|
|
540
|
+
$location = Utils::getPointFromMixed(data_get($values, 'location'));
|
|
541
|
+
$hasZeroLocation = $existingLocation instanceof SpatialPoint
|
|
542
|
+
&& $existingLocation->getLat() == 0.0
|
|
543
|
+
&& $existingLocation->getLng() == 0.0;
|
|
544
|
+
|
|
545
|
+
if (($existingLocation === null || $hasZeroLocation) && $location instanceof SpatialPoint) {
|
|
546
|
+
$updates['location'] = $location;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!empty($updates)) {
|
|
550
|
+
$existingPlace->fill($updates);
|
|
551
|
+
$existingPlace->save();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return $existingPlace;
|
|
555
|
+
}
|
|
556
|
+
|
|
416
557
|
/**
|
|
417
558
|
* Create a new Place instance from a geocoding lookup.
|
|
418
559
|
*
|
|
@@ -571,31 +712,33 @@ class Place extends Model
|
|
|
571
712
|
}
|
|
572
713
|
|
|
573
714
|
// Get public_id if supplied if set
|
|
574
|
-
$id = data_get($place, 'id')
|
|
715
|
+
$id = data_get($place, 'id') ?: data_get($place, 'public_id');
|
|
575
716
|
|
|
576
717
|
// If $place has a valid uuid and a matching Place object exists, return the uuid
|
|
577
718
|
if (Utils::isPublicId($id) && $existingPlace = static::where('public_id', $id)->first()) {
|
|
578
719
|
return $existingPlace;
|
|
579
720
|
}
|
|
580
721
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
722
|
+
$query = static::composeGeocodingQuery($place)
|
|
723
|
+
?? static::normalizePlaceValue(data_get($place, 'address'))
|
|
724
|
+
?? static::normalizePlaceValue(data_get($place, 'street1'));
|
|
725
|
+
|
|
726
|
+
$geocoded = [];
|
|
727
|
+
if ($query) {
|
|
728
|
+
$geocoded = static::getValuesFromGeocodingLookup($query);
|
|
585
729
|
}
|
|
586
730
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if ($
|
|
590
|
-
return static::
|
|
731
|
+
$values = static::mergeStructuredPlaceAttributes($place, $geocoded);
|
|
732
|
+
|
|
733
|
+
if ($existingPlace = static::findExistingSharedPlace($values)) {
|
|
734
|
+
return static::hydrateSharedPlace($existingPlace, $values);
|
|
591
735
|
}
|
|
592
736
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
$place['location'] = new SpatialPoint(0, 0);
|
|
737
|
+
if (empty($values['location'])) {
|
|
738
|
+
$values['location'] = new SpatialPoint(0, 0);
|
|
596
739
|
}
|
|
597
740
|
|
|
598
|
-
return static::create($
|
|
741
|
+
return static::create($values);
|
|
599
742
|
}
|
|
600
743
|
// If $place is a GoogleAddress object
|
|
601
744
|
elseif ($place instanceof \Geocoder\Provider\GoogleMaps\Model\GoogleAddress) {
|
|
@@ -655,7 +798,11 @@ class Place extends Model
|
|
|
655
798
|
}
|
|
656
799
|
}
|
|
657
800
|
|
|
658
|
-
$values = $place;
|
|
801
|
+
$values = is_array($place) ? $place : (array) $place;
|
|
802
|
+
|
|
803
|
+
if ($existingPlace = static::findExistingSharedPlace($values)) {
|
|
804
|
+
return $existingPlace->uuid;
|
|
805
|
+
}
|
|
659
806
|
|
|
660
807
|
// create a new place
|
|
661
808
|
return static::insertGetUuid((array) $values);
|
|
@@ -703,21 +850,8 @@ class Place extends Model
|
|
|
703
850
|
$values['location'] = Utils::parsePointToWkt($values['location']);
|
|
704
851
|
}
|
|
705
852
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
->select(['uuid'])->where([
|
|
709
|
-
'company_uuid' => session('company'),
|
|
710
|
-
'name' => $values['name'] ?? null,
|
|
711
|
-
'street1' => $values['street1'] ?? null,
|
|
712
|
-
])
|
|
713
|
-
->whereNull('deleted_at')
|
|
714
|
-
->first();
|
|
715
|
-
|
|
716
|
-
if ($existing) {
|
|
717
|
-
unset($values['uuid'], $values['created_at'], $values['_key'], $values['company_uuid']);
|
|
718
|
-
static::where('uuid', $existing->uuid)->update($values);
|
|
719
|
-
|
|
720
|
-
return $existing->uuid;
|
|
853
|
+
if ($existing = static::findExistingSharedPlace($values)) {
|
|
854
|
+
return static::hydrateSharedPlace($existing, $values)->uuid;
|
|
721
855
|
}
|
|
722
856
|
|
|
723
857
|
if (isset($values['meta']) && (is_object($values['meta']) || is_array($values['meta']))) {
|
|
@@ -229,7 +229,7 @@ class ServiceRate extends Model
|
|
|
229
229
|
*/
|
|
230
230
|
public function isAlgorithm(): bool
|
|
231
231
|
{
|
|
232
|
-
return $this->rate_calculation_method === 'algo';
|
|
232
|
+
return $this->rate_calculation_method === 'algo' || $this->rate_calculation_method === 'algorithm';
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
/**
|
|
@@ -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
|
}
|
|
@@ -640,7 +677,7 @@ class ServiceRate extends Model
|
|
|
640
677
|
*
|
|
641
678
|
* @return array an array containing the calculated quote and line items
|
|
642
679
|
*/
|
|
643
|
-
public function quoteFromPreliminaryData($entities = [], $waypoints = [], ?int $totalDistance = 0, ?int $totalTime = 0, ?bool $isCashOnDelivery = false)
|
|
680
|
+
public function quoteFromPreliminaryData($entities = [], $waypoints = [], ?int $totalDistance = 0, ?int $totalTime = 0, ?bool $isCashOnDelivery = false, ?int $endpointCount = null)
|
|
644
681
|
{
|
|
645
682
|
$lines = collect();
|
|
646
683
|
$subTotal = data_get($this, 'base_fee', 0);
|
|
@@ -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,24 @@ class ServiceRate extends Model
|
|
|
702
739
|
}
|
|
703
740
|
|
|
704
741
|
if ($this->isAlgorithm()) {
|
|
705
|
-
$
|
|
742
|
+
$resolvedEndpointCount = $endpointCount ?? $this->inferEndpointCountFromStops($waypoints);
|
|
743
|
+
$rateFee = $this->normalizeCalculatedMoney(Algo::exec(
|
|
706
744
|
$this->algorithm,
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
745
|
+
$this->buildAlgorithmVariables(
|
|
746
|
+
$entities,
|
|
747
|
+
$waypoints,
|
|
748
|
+
$totalDistance,
|
|
749
|
+
$totalTime,
|
|
750
|
+
$resolvedEndpointCount
|
|
751
|
+
),
|
|
711
752
|
true
|
|
712
|
-
);
|
|
753
|
+
));
|
|
713
754
|
|
|
714
|
-
$subTotal +=
|
|
755
|
+
$subTotal += $rateFee;
|
|
715
756
|
|
|
716
757
|
$lines->push([
|
|
717
758
|
'details' => 'Service Fee',
|
|
718
|
-
'amount' =>
|
|
759
|
+
'amount' => $rateFee,
|
|
719
760
|
'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
|
|
720
761
|
'currency' => $this->currency,
|
|
721
762
|
'code' => 'BASE_FEE',
|
|
@@ -758,9 +799,10 @@ class ServiceRate extends Model
|
|
|
758
799
|
}
|
|
759
800
|
|
|
760
801
|
$subTotal += $serviceParcelFee->fee;
|
|
802
|
+
$parcelFeeName = ucwords(str_replace(['_', '-'], ' ', data_get($serviceParcelFee, 'size', 'parcel')));
|
|
761
803
|
|
|
762
804
|
$lines->push([
|
|
763
|
-
'details' => $
|
|
805
|
+
'details' => $parcelFeeName . ' parcel fee',
|
|
764
806
|
'amount' => Utils::numbersOnly($serviceParcelFee->fee),
|
|
765
807
|
'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
|
|
766
808
|
'currency' => $this->currency,
|
|
@@ -777,7 +819,7 @@ class ServiceRate extends Model
|
|
|
777
819
|
if ($this->hasCodFlatFee()) {
|
|
778
820
|
$subTotal += $codFee = $this->cod_flat_fee;
|
|
779
821
|
} elseif ($this->hasCodPercentageFee()) {
|
|
780
|
-
$subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
|
|
822
|
+
$subTotal += $codFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->cod_percent, $baseRate));
|
|
781
823
|
}
|
|
782
824
|
|
|
783
825
|
$lines->push([
|
|
@@ -794,7 +836,7 @@ class ServiceRate extends Model
|
|
|
794
836
|
if ($this->hasPeakHoursFlatFee()) {
|
|
795
837
|
$subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
|
|
796
838
|
} elseif ($this->hasPeakHoursPercentageFee()) {
|
|
797
|
-
$subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
|
|
839
|
+
$subTotal += $peakHoursFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->peak_hours_percent, $baseRate));
|
|
798
840
|
}
|
|
799
841
|
|
|
800
842
|
$lines->push([
|
|
@@ -872,13 +914,13 @@ class ServiceRate extends Model
|
|
|
872
914
|
}
|
|
873
915
|
|
|
874
916
|
if ($this->isPerMeter()) {
|
|
875
|
-
$perMeterDistance = $this->
|
|
876
|
-
$rateFee = $perMeterDistance * $this->per_meter_flat_rate_fee;
|
|
917
|
+
$perMeterDistance = $this->normalizeDistanceForUnit($totalDistance, $this->per_meter_unit);
|
|
918
|
+
$rateFee = $this->normalizeCalculatedMoney($perMeterDistance * $this->per_meter_flat_rate_fee);
|
|
877
919
|
$subTotal += $rateFee;
|
|
878
920
|
|
|
879
921
|
$lines->push([
|
|
880
922
|
'details' => 'Service Fee',
|
|
881
|
-
'amount' =>
|
|
923
|
+
'amount' => $rateFee,
|
|
882
924
|
'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
|
|
883
925
|
'currency' => $this->currency,
|
|
884
926
|
'code' => 'BASE_FEE',
|
|
@@ -886,20 +928,23 @@ class ServiceRate extends Model
|
|
|
886
928
|
}
|
|
887
929
|
|
|
888
930
|
if ($this->isAlgorithm()) {
|
|
889
|
-
$rateFee = Algo::exec(
|
|
931
|
+
$rateFee = $this->normalizeCalculatedMoney(Algo::exec(
|
|
890
932
|
$this->algorithm,
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
933
|
+
$this->buildAlgorithmVariables(
|
|
934
|
+
$payload->entities->all(),
|
|
935
|
+
$waypoints->all(),
|
|
936
|
+
$totalDistance,
|
|
937
|
+
$totalTime,
|
|
938
|
+
(int) ($payload->pickup ? 1 : 0) + (int) ($payload->dropoff ? 1 : 0)
|
|
939
|
+
),
|
|
895
940
|
true
|
|
896
|
-
);
|
|
941
|
+
));
|
|
897
942
|
|
|
898
|
-
$subTotal +=
|
|
943
|
+
$subTotal += $rateFee;
|
|
899
944
|
|
|
900
945
|
$lines->push([
|
|
901
946
|
'details' => 'Service Fee',
|
|
902
|
-
'amount' =>
|
|
947
|
+
'amount' => $rateFee,
|
|
903
948
|
'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
|
|
904
949
|
'currency' => $this->currency,
|
|
905
950
|
'code' => 'BASE_FEE',
|
|
@@ -942,9 +987,10 @@ class ServiceRate extends Model
|
|
|
942
987
|
}
|
|
943
988
|
|
|
944
989
|
$subTotal += $serviceParcelFee->fee;
|
|
990
|
+
$parcelFeeName = ucwords(str_replace(['_', '-'], ' ', data_get($serviceParcelFee, 'size', 'parcel')));
|
|
945
991
|
|
|
946
992
|
$lines->push([
|
|
947
|
-
'details' => $
|
|
993
|
+
'details' => $parcelFeeName . ' parcel fee',
|
|
948
994
|
'amount' => Utils::numbersOnly($serviceParcelFee->fee),
|
|
949
995
|
'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
|
|
950
996
|
'currency' => $this->currency,
|
|
@@ -961,7 +1007,7 @@ class ServiceRate extends Model
|
|
|
961
1007
|
if ($this->hasCodFlatFee()) {
|
|
962
1008
|
$subTotal += $codFee = $this->cod_flat_fee;
|
|
963
1009
|
} elseif ($this->hasCodPercentageFee()) {
|
|
964
|
-
$subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
|
|
1010
|
+
$subTotal += $codFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->cod_percent, $baseRate));
|
|
965
1011
|
}
|
|
966
1012
|
|
|
967
1013
|
$lines->push([
|
|
@@ -978,7 +1024,7 @@ class ServiceRate extends Model
|
|
|
978
1024
|
if ($this->hasPeakHoursFlatFee()) {
|
|
979
1025
|
$subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
|
|
980
1026
|
} elseif ($this->hasPeakHoursPercentageFee()) {
|
|
981
|
-
$subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
|
|
1027
|
+
$subTotal += $peakHoursFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->peak_hours_percent, $baseRate));
|
|
982
1028
|
}
|
|
983
1029
|
|
|
984
1030
|
$lines->push([
|
|
@@ -993,6 +1039,53 @@ class ServiceRate extends Model
|
|
|
993
1039
|
return [$subTotal, $lines];
|
|
994
1040
|
}
|
|
995
1041
|
|
|
1042
|
+
protected function normalizeDistanceForUnit(?int $distanceInMeters = 0, ?string $unit = 'm'): float|int
|
|
1043
|
+
{
|
|
1044
|
+
$distanceInMeters = (float) ($distanceInMeters ?? 0);
|
|
1045
|
+
|
|
1046
|
+
return match ($unit) {
|
|
1047
|
+
'km' => $distanceInMeters / 1000,
|
|
1048
|
+
'ft' => $distanceInMeters / 0.3048,
|
|
1049
|
+
'yd' => $distanceInMeters / 0.9144,
|
|
1050
|
+
'mi' => $distanceInMeters / 1609.344,
|
|
1051
|
+
default => $distanceInMeters,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
protected function normalizeCalculatedMoney($amount = 0): int
|
|
1056
|
+
{
|
|
1057
|
+
return (int) round((float) ($amount ?? 0));
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
protected function buildAlgorithmVariables($entities = [], $stops = [], ?int $totalDistance = 0, ?int $totalTime = 0, int $endpointCount = 0): array
|
|
1061
|
+
{
|
|
1062
|
+
$entityCollection = collect($entities)->filter();
|
|
1063
|
+
$stopCount = collect($stops)->filter()->count();
|
|
1064
|
+
$endpointCount = max(0, min($endpointCount, $stopCount));
|
|
1065
|
+
$waypointCount = max($stopCount - $endpointCount, 0);
|
|
1066
|
+
|
|
1067
|
+
return Algo::normalizeVariables([
|
|
1068
|
+
'distance_m' => $totalDistance ?? 0,
|
|
1069
|
+
'time_s' => $totalTime ?? 0,
|
|
1070
|
+
'stops' => $stopCount,
|
|
1071
|
+
'waypoints' => $waypointCount,
|
|
1072
|
+
'parcels' => $entityCollection->where('type', 'parcel')->count(),
|
|
1073
|
+
'entities' => $entityCollection->count(),
|
|
1074
|
+
'base_fee' => Utils::numbersOnly($this->base_fee ?? 0),
|
|
1075
|
+
]);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
protected function inferEndpointCountFromStops($stops = []): int
|
|
1079
|
+
{
|
|
1080
|
+
$stopCount = collect($stops)->filter()->count();
|
|
1081
|
+
|
|
1082
|
+
if ($stopCount <= 1) {
|
|
1083
|
+
return $stopCount;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return 2;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
996
1089
|
/**
|
|
997
1090
|
* Find the ServiceRateFee based on the total distance.
|
|
998
1091
|
*
|
|
@@ -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
|
}
|