@fleetbase/fleetops-engine 0.6.44 → 0.6.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/addon/components/entity/form.hbs +24 -0
  2. package/addon/components/entity/form.js +30 -0
  3. package/addon/components/leaflet-draw-control.js +1 -1
  4. package/addon/components/map/drawer/geofence-event-listing.js +5 -2
  5. package/addon/components/order/form/service-rate.hbs +1 -1
  6. package/addon/components/service-rate/details.hbs +6 -6
  7. package/addon/components/service-rate/form.hbs +28 -15
  8. package/addon/controllers/operations/orders/index/details/virtual.js +5 -0
  9. package/addon/controllers/operations/orders/index/details.js +27 -2
  10. package/addon/controllers/operations/service-rates/index/edit.js +1 -0
  11. package/addon/controllers/operations/service-rates/index/new.js +1 -0
  12. package/addon/helpers/f-to-int.js +6 -0
  13. package/addon/routes/application.js +3 -3
  14. package/addon/routes/operations/orders/index/details/virtual.js +1 -0
  15. package/addon/routes/operations/orders/index/details.js +19 -3
  16. package/addon/services/equipment-actions.js +4 -6
  17. package/addon/services/leaflet-map-manager.js +4 -0
  18. package/addon/services/maintenance-actions.js +4 -6
  19. package/addon/services/part-actions.js +4 -6
  20. package/addon/services/service-rate-actions.js +8 -3
  21. package/addon/templates/operations/orders/index/details/virtual.hbs +7 -1
  22. package/addon/utils/fleet-ops-options.js +33 -0
  23. package/addon/utils/serialize-payload.js +1 -1
  24. package/app/controllers/operations/orders/index/details/virtual.js +1 -0
  25. package/app/helpers/f-to-int.js +1 -0
  26. package/composer.json +1 -1
  27. package/extension.json +1 -1
  28. package/package.json +2 -2
  29. package/server/src/Http/Controllers/Api/v1/OrderController.php +5 -5
  30. package/server/src/Http/Controllers/Api/v1/PurchaseRateController.php +11 -3
  31. package/server/src/Http/Controllers/Api/v1/ServiceQuoteController.php +4 -0
  32. package/server/src/Http/Controllers/Api/v1/ServiceRateController.php +6 -1
  33. package/server/src/Http/Requests/CreateServiceRateRequest.php +2 -2
  34. package/server/src/Http/Resources/v1/Order.php +0 -1
  35. package/server/src/Http/Resources/v1/ServiceQuote.php +2 -2
  36. package/server/src/Models/Entity.php +7 -0
  37. package/server/src/Models/Order.php +39 -7
  38. package/server/src/Models/ServiceRate.php +122 -30
  39. package/server/src/Observers/PurchaseRateObserver.php +3 -5
  40. package/server/src/Rules/ComputableAlgo.php +1 -3
  41. package/server/src/Support/Algo.php +87 -1
  42. package/server/src/Support/Utils.php +35 -0
@@ -321,7 +321,7 @@ class OrderController extends Controller
321
321
  }
322
322
 
323
323
  // load required relations
324
- $order->load(['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate', 'customer', 'facilitator']);
324
+ $order->load(['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate.serviceQuote.items', 'customer', 'facilitator']);
325
325
 
326
326
  // Determine if order should be dispatched on creation
327
327
  $shouldDispatch = $request->boolean('dispatch') && $integratedVendorOrder === null;
@@ -364,7 +364,7 @@ class OrderController extends Controller
364
364
 
365
365
  // find for the order
366
366
  try {
367
- $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate', 'customer', 'facilitator']);
367
+ $order = Order::findRecordOrFail($id, ['trackingNumber', 'driverAssigned', 'purchaseRate.serviceQuote.items', 'customer', 'facilitator']);
368
368
  } catch (ModelNotFoundException $exception) {
369
369
  return response()->json(
370
370
  [
@@ -549,7 +549,7 @@ class OrderController extends Controller
549
549
  $order->flushAttributesCache();
550
550
 
551
551
  // load required relations
552
- $order->load(['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate', 'customer', 'facilitator']);
552
+ $order->load(['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate.serviceQuote.items', 'customer', 'facilitator']);
553
553
 
554
554
  // response the order resource
555
555
  return new OrderResource($order);
@@ -759,7 +759,7 @@ class OrderController extends Controller
759
759
  {
760
760
  // find for the order
761
761
  try {
762
- $order = Order::findRecordOrFail($id, ['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate', 'customer', 'facilitator']);
762
+ $order = Order::findRecordOrFail($id, ['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate.serviceQuote.items', 'customer', 'facilitator']);
763
763
  } catch (ModelNotFoundException $exception) {
764
764
  return response()->json(
765
765
  [
@@ -839,7 +839,7 @@ class OrderController extends Controller
839
839
  public function dispatchOrder(string $id)
840
840
  {
841
841
  try {
842
- $order = Order::findRecordOrFail($id, ['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate', 'customer', 'facilitator']);
842
+ $order = Order::findRecordOrFail($id, ['trackingNumber', 'trackingStatuses', 'driverAssigned', 'vehicleAssigned', 'purchaseRate.serviceQuote.items', 'customer', 'facilitator']);
843
843
  } catch (ModelNotFoundException $exception) {
844
844
  return response()->json(
845
845
  [
@@ -41,10 +41,19 @@ class PurchaseRateController extends Controller
41
41
 
42
42
  // order assignment
43
43
  if ($request->has('order')) {
44
- $input['order_uuid'] = Utils::getUuid('orders', [
44
+ $orderUuid = Utils::getUuid('orders', [
45
45
  'public_id' => $request->input('order'),
46
46
  'company_uuid' => session('company'),
47
47
  ]);
48
+
49
+ $order = Order::where('uuid', $orderUuid)->first();
50
+
51
+ if ($order instanceof Order) {
52
+ $input['payload_uuid'] = $order->payload_uuid;
53
+ $input['customer_uuid'] = $order->customer_uuid;
54
+ $input['customer_type'] = $order->customer_type;
55
+ $input['company_uuid'] = $order->company_uuid ?? $input['company_uuid'];
56
+ }
48
57
  } elseif ($createOrder) {
49
58
  // create order from service quote
50
59
  $serviceQuote = ServiceQuote::where('uuid', $input['service_quote_uuid'])->orWhere('public_id', $request->input('service_quote'))->first();
@@ -70,9 +79,8 @@ class PurchaseRateController extends Controller
70
79
  // create the purchaseRate
71
80
  $purchaseRate = PurchaseRate::create($input);
72
81
 
73
- // set purchase rate to order
74
82
  if ($order instanceof Order) {
75
- $order->update(['purchase_rate_uuid' => $purchaseRate->uuid]);
83
+ $order->attachPurchaseRate($purchaseRate);
76
84
  }
77
85
 
78
86
  // response the driver resource
@@ -98,6 +98,7 @@ class ServiceQuoteController extends Controller
98
98
  'amount' => $subTotal,
99
99
  'currency' => $serviceRate->currency,
100
100
  ]);
101
+ $quote->setRelation('serviceRate', $serviceRate);
101
102
 
102
103
  $items = $lines->map(function ($line) use ($quote) {
103
104
  return ServiceQuoteItem::create([
@@ -143,6 +144,7 @@ class ServiceQuoteController extends Controller
143
144
  'amount' => $subTotal,
144
145
  'currency' => $serviceRate->currency,
145
146
  ]);
147
+ $quote->setRelation('serviceRate', $serviceRate);
146
148
 
147
149
  $items = $lines->map(function ($line) use ($quote) {
148
150
  return ServiceQuoteItem::create([
@@ -288,6 +290,7 @@ class ServiceQuoteController extends Controller
288
290
  'amount' => $subTotal,
289
291
  'currency' => $serviceRate->currency,
290
292
  ]);
293
+ $quote->setRelation('serviceRate', $serviceRate);
291
294
 
292
295
  // set preliminary data to meta
293
296
  $quote->updateMeta('preliminary_data', $preliminaryData);
@@ -335,6 +338,7 @@ class ServiceQuoteController extends Controller
335
338
  'amount' => $subTotal,
336
339
  'currency' => $serviceRate->currency,
337
340
  ]);
341
+ $quote->setRelation('serviceRate', $serviceRate);
338
342
 
339
343
  // set preliminary data to meta
340
344
  $quote->updateMeta('preliminary_data', $preliminaryData);
@@ -30,6 +30,8 @@ class ServiceRateController extends Controller
30
30
  'rate_calculation_method',
31
31
  'currency',
32
32
  'base_fee',
33
+ 'max_distance_unit',
34
+ 'max_distance',
33
35
  'per_meter_unit',
34
36
  'per_meter_flat_rate_fee',
35
37
  'meter_fees',
@@ -119,7 +121,10 @@ class ServiceRateController extends Controller
119
121
  'rate_calculation_method',
120
122
  'currency',
121
123
  'base_fee',
122
- 'per_km_flat_rate_fee',
124
+ 'max_distance_unit',
125
+ 'max_distance',
126
+ 'per_meter_unit',
127
+ 'per_meter_flat_rate_fee',
123
128
  'meter_fees',
124
129
  'meter_fees.*.distance',
125
130
  'meter_fees.*.fee',
@@ -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']);
@@ -45,7 +45,6 @@ class Order extends FleetbaseResource
45
45
  'tracking_number_uuid' => $this->when($isInternal, $this->tracking_number_uuid),
46
46
  'driver_assigned_uuid' => $this->when($isInternal, $this->driver_assigned_uuid),
47
47
  'vehicle_assigned_uuid'=> $this->when($isInternal, $this->vehicle_assigned_uuid),
48
- 'service_quote_uuid' => $this->when($isInternal, $this->service_quote_uuid),
49
48
  'has_driver_assigned' => $this->when($isInternal, $this->has_driver_assigned),
50
49
  'is_scheduled' => $this->when($isInternal, $this->is_scheduled),
51
50
  'order_config_uuid' => $this->when($isInternal, $this->order_config_uuid),
@@ -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',
@@ -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 (isset($serviceRateParcelFees[$i]['uuid'])) {
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
- ServiceRateParcelFee::bulkInsert($serviceRateParcelFees);
420
+
421
+ $existingParcelFeesQuery = ServiceRateParcelFee::where('service_rate_uuid', $this->uuid);
422
+
423
+ if (!empty($submittedUuids)) {
424
+ $existingParcelFeesQuery->whereNotIn('uuid', $submittedUuids)->delete();
425
+ } else {
426
+ $existingParcelFeesQuery->delete();
427
+ }
428
+
429
+ if (!empty($serviceRateParcelFees)) {
430
+ ServiceRateParcelFee::bulkInsert($serviceRateParcelFees);
431
+ }
395
432
 
396
433
  return $this;
397
434
  }
@@ -688,13 +725,13 @@ class ServiceRate extends Model
688
725
  }
689
726
 
690
727
  if ($this->isPerMeter()) {
691
- $perMeterDistance = $this->per_meter_unit === 'km' ? round($totalDistance / 1000) : $totalDistance;
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' => Utils::numbersOnly($rateFee),
734
+ 'amount' => $rateFee,
698
735
  'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
699
736
  'currency' => $this->currency,
700
737
  'code' => 'BASE_FEE',
@@ -702,20 +739,23 @@ class ServiceRate extends Model
702
739
  }
703
740
 
704
741
  if ($this->isAlgorithm()) {
705
- $rateFee = Algo::exec(
742
+ $rateFee = $this->normalizeCalculatedMoney(Algo::exec(
706
743
  $this->algorithm,
707
- [
708
- 'distance' => $totalDistance,
709
- 'time' => $totalTime,
710
- ],
744
+ $this->buildAlgorithmVariables(
745
+ $entities,
746
+ $waypoints,
747
+ $totalDistance,
748
+ $totalTime,
749
+ $this->inferEndpointCountFromStops($waypoints)
750
+ ),
711
751
  true
712
- );
752
+ ));
713
753
 
714
- $subTotal += Utils::numbersOnly($rateFee);
754
+ $subTotal += $rateFee;
715
755
 
716
756
  $lines->push([
717
757
  'details' => 'Service Fee',
718
- 'amount' => Utils::numbersOnly($rateFee),
758
+ 'amount' => $rateFee,
719
759
  'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
720
760
  'currency' => $this->currency,
721
761
  'code' => 'BASE_FEE',
@@ -758,9 +798,10 @@ class ServiceRate extends Model
758
798
  }
759
799
 
760
800
  $subTotal += $serviceParcelFee->fee;
801
+ $parcelFeeName = ucwords(str_replace(['_', '-'], ' ', data_get($serviceParcelFee, 'size', 'parcel')));
761
802
 
762
803
  $lines->push([
763
- 'details' => $serviceParcelFee->name . ' parcel fee',
804
+ 'details' => $parcelFeeName . ' parcel fee',
764
805
  'amount' => Utils::numbersOnly($serviceParcelFee->fee),
765
806
  'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
766
807
  'currency' => $this->currency,
@@ -777,7 +818,7 @@ class ServiceRate extends Model
777
818
  if ($this->hasCodFlatFee()) {
778
819
  $subTotal += $codFee = $this->cod_flat_fee;
779
820
  } elseif ($this->hasCodPercentageFee()) {
780
- $subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
821
+ $subTotal += $codFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->cod_percent, $baseRate));
781
822
  }
782
823
 
783
824
  $lines->push([
@@ -794,7 +835,7 @@ class ServiceRate extends Model
794
835
  if ($this->hasPeakHoursFlatFee()) {
795
836
  $subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
796
837
  } elseif ($this->hasPeakHoursPercentageFee()) {
797
- $subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
838
+ $subTotal += $peakHoursFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->peak_hours_percent, $baseRate));
798
839
  }
799
840
 
800
841
  $lines->push([
@@ -872,13 +913,13 @@ class ServiceRate extends Model
872
913
  }
873
914
 
874
915
  if ($this->isPerMeter()) {
875
- $perMeterDistance = $this->per_meter_unit === 'km' ? round($totalDistance / 1000) : $totalDistance;
876
- $rateFee = $perMeterDistance * $this->per_meter_flat_rate_fee;
916
+ $perMeterDistance = $this->normalizeDistanceForUnit($totalDistance, $this->per_meter_unit);
917
+ $rateFee = $this->normalizeCalculatedMoney($perMeterDistance * $this->per_meter_flat_rate_fee);
877
918
  $subTotal += $rateFee;
878
919
 
879
920
  $lines->push([
880
921
  'details' => 'Service Fee',
881
- 'amount' => Utils::numbersOnly($rateFee),
922
+ 'amount' => $rateFee,
882
923
  'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
883
924
  'currency' => $this->currency,
884
925
  'code' => 'BASE_FEE',
@@ -886,20 +927,23 @@ class ServiceRate extends Model
886
927
  }
887
928
 
888
929
  if ($this->isAlgorithm()) {
889
- $rateFee = Algo::exec(
930
+ $rateFee = $this->normalizeCalculatedMoney(Algo::exec(
890
931
  $this->algorithm,
891
- [
892
- 'distance' => $totalDistance,
893
- 'time' => $totalTime,
894
- ],
932
+ $this->buildAlgorithmVariables(
933
+ $payload->entities->all(),
934
+ $waypoints->all(),
935
+ $totalDistance,
936
+ $totalTime,
937
+ (int) ($payload->pickup ? 1 : 0) + (int) ($payload->dropoff ? 1 : 0)
938
+ ),
895
939
  true
896
- );
940
+ ));
897
941
 
898
- $subTotal += Utils::numbersOnly($rateFee);
942
+ $subTotal += $rateFee;
899
943
 
900
944
  $lines->push([
901
945
  'details' => 'Service Fee',
902
- 'amount' => Utils::numbersOnly($rateFee),
946
+ 'amount' => $rateFee,
903
947
  'formatted_amount' => Utils::moneyFormat($rateFee, $this->currency),
904
948
  'currency' => $this->currency,
905
949
  'code' => 'BASE_FEE',
@@ -942,9 +986,10 @@ class ServiceRate extends Model
942
986
  }
943
987
 
944
988
  $subTotal += $serviceParcelFee->fee;
989
+ $parcelFeeName = ucwords(str_replace(['_', '-'], ' ', data_get($serviceParcelFee, 'size', 'parcel')));
945
990
 
946
991
  $lines->push([
947
- 'details' => $serviceParcelFee->name . ' parcel fee',
992
+ 'details' => $parcelFeeName . ' parcel fee',
948
993
  'amount' => Utils::numbersOnly($serviceParcelFee->fee),
949
994
  'formatted_amount' => Utils::moneyFormat($serviceParcelFee->fee, $this->currency),
950
995
  'currency' => $this->currency,
@@ -961,7 +1006,7 @@ class ServiceRate extends Model
961
1006
  if ($this->hasCodFlatFee()) {
962
1007
  $subTotal += $codFee = $this->cod_flat_fee;
963
1008
  } elseif ($this->hasCodPercentageFee()) {
964
- $subTotal += $codFee = Utils::calculatePercentage($this->cod_percent, $baseRate);
1009
+ $subTotal += $codFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->cod_percent, $baseRate));
965
1010
  }
966
1011
 
967
1012
  $lines->push([
@@ -978,7 +1023,7 @@ class ServiceRate extends Model
978
1023
  if ($this->hasPeakHoursFlatFee()) {
979
1024
  $subTotal += $peakHoursFee = $this->peak_hours_flat_fee;
980
1025
  } elseif ($this->hasPeakHoursPercentageFee()) {
981
- $subTotal += $peakHoursFee = Utils::calculatePercentage($this->peak_hours_percent, $baseRate);
1026
+ $subTotal += $peakHoursFee = $this->normalizeCalculatedMoney(Utils::calculatePercentage($this->peak_hours_percent, $baseRate));
982
1027
  }
983
1028
 
984
1029
  $lines->push([
@@ -993,6 +1038,53 @@ class ServiceRate extends Model
993
1038
  return [$subTotal, $lines];
994
1039
  }
995
1040
 
1041
+ protected function normalizeDistanceForUnit(?int $distanceInMeters = 0, ?string $unit = 'm'): float|int
1042
+ {
1043
+ $distanceInMeters = (float) ($distanceInMeters ?? 0);
1044
+
1045
+ return match ($unit) {
1046
+ 'km' => $distanceInMeters / 1000,
1047
+ 'ft' => $distanceInMeters / 0.3048,
1048
+ 'yd' => $distanceInMeters / 0.9144,
1049
+ 'mi' => $distanceInMeters / 1609.344,
1050
+ default => $distanceInMeters,
1051
+ };
1052
+ }
1053
+
1054
+ protected function normalizeCalculatedMoney($amount = 0): int
1055
+ {
1056
+ return (int) round((float) ($amount ?? 0));
1057
+ }
1058
+
1059
+ protected function buildAlgorithmVariables($entities = [], $stops = [], ?int $totalDistance = 0, ?int $totalTime = 0, int $endpointCount = 0): array
1060
+ {
1061
+ $entityCollection = collect($entities)->filter();
1062
+ $stopCount = collect($stops)->filter()->count();
1063
+ $endpointCount = max(0, min($endpointCount, $stopCount));
1064
+ $waypointCount = max($stopCount - $endpointCount, 0);
1065
+
1066
+ return Algo::normalizeVariables([
1067
+ 'distance_m' => $totalDistance ?? 0,
1068
+ 'time_s' => $totalTime ?? 0,
1069
+ 'stops' => $stopCount,
1070
+ 'waypoints' => $waypointCount,
1071
+ 'parcels' => $entityCollection->where('type', 'parcel')->count(),
1072
+ 'entities' => $entityCollection->count(),
1073
+ 'base_fee' => Utils::numbersOnly($this->base_fee ?? 0),
1074
+ ]);
1075
+ }
1076
+
1077
+ protected function inferEndpointCountFromStops($stops = []): int
1078
+ {
1079
+ $stopCount = collect($stops)->filter()->count();
1080
+
1081
+ if ($stopCount <= 1) {
1082
+ return $stopCount;
1083
+ }
1084
+
1085
+ return 2;
1086
+ }
1087
+
996
1088
  /**
997
1089
  * Find the ServiceRateFee based on the total distance.
998
1090
  *
@@ -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', $company->country ? Utils::getCurrenyFromCountryCode($company->country) : 'SGD');
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
  }
@@ -16,9 +16,7 @@ class ComputableAlgo implements Rule
16
16
  */
17
17
  public function passes($attribute, $value)
18
18
  {
19
- $distanceAndTime = Algo::calculateDrivingDistanceAndTime('1.3506853', '103.87199110000006', '1.3621663', '103.88450490000002');
20
-
21
- return Algo::exec($value, $distanceAndTime) > 0;
19
+ return Algo::isComputable($value);
22
20
  }
23
21
 
24
22
  /**