@fleetbase/fleetops-engine 0.6.15 → 0.6.17

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.
@@ -435,7 +435,7 @@ export default class OrderConfigManagerActivityFlowComponent extends Component {
435
435
  * Initializes the activity flow by either loading from configuration or creating a default flow.
436
436
  */
437
437
  initializeActivityFlow() {
438
- const hasFlow = Object.keys(this.config.flow).length > 0;
438
+ const hasFlow = this.config && Object.keys(this.config.flow).length > 0;
439
439
  if (hasFlow) {
440
440
  this.deserializeFlow(this.config.flow);
441
441
  this.initializeContext();
@@ -29,9 +29,13 @@ export default class OrderConfigManagerDetailsComponent extends Component {
29
29
  * @param {Object} owner - The owner of the component.
30
30
  * @param {Object} args - The arguments passed to the component, including the configuration.
31
31
  */
32
- constructor(owner, { config }) {
32
+ constructor(owner, { config, configManagerContext }) {
33
33
  super(...arguments);
34
34
  this.config = config;
35
+
36
+ configManagerContext.on('onConfigChanged', (newConfig) => {
37
+ this.changeConfig(newConfig);
38
+ });
35
39
  }
36
40
 
37
41
  /**
@@ -87,11 +91,23 @@ export default class OrderConfigManagerDetailsComponent extends Component {
87
91
  @task *deleteConfig() {
88
92
  try {
89
93
  yield this.config.destroyRecord();
94
+ this.notifications.success(`Order config ${this.config.name} was deleted.`);
95
+
90
96
  if (typeof this.args.onConfigDeleted === 'function') {
91
- this.args.onConfigDeleted(this.config);
97
+ this.args.onConfigDeleted();
92
98
  }
93
99
  } catch (error) {
94
100
  this.notifications.serverError(error);
95
101
  }
96
102
  }
103
+
104
+ /**
105
+ * Handle change of config.
106
+ *
107
+ * @param {OrderConfigModel} newConfig
108
+ * @memberof OrderConfigManagerDetailsComponent
109
+ */
110
+ changeConfig(newConfig) {
111
+ this.config = newConfig;
112
+ }
97
113
  }
@@ -20,6 +20,7 @@
20
20
  this.tab.component
21
21
  configManagerContext=this.configManagerContext
22
22
  config=this.currentConfig
23
+ allConfigs=this.configs
23
24
  context=this.context
24
25
  contextModel=this.contextModel
25
26
  tabOptions=this.tab
@@ -31,5 +32,7 @@
31
32
  )
32
33
  onTabChanged=this.onTabChanged
33
34
  onClickCreateOrderConfig=this.createNewOrderConfig
35
+ childTaskRunning=this.childTaskRunning
36
+ configManagerContext=this.configManagerContext
34
37
  )
35
38
  }}
@@ -31,6 +31,7 @@ export default class OrderConfigManagerComponent extends Component {
31
31
  @tracked context;
32
32
  @tracked contextModel;
33
33
  @tracked ready = false;
34
+ @tracked childTaskRunning = false;
34
35
 
35
36
  /**
36
37
  * Returns the array of tabs available for the panel.
@@ -62,11 +63,36 @@ export default class OrderConfigManagerComponent extends Component {
62
63
 
63
64
  this.context = context;
64
65
  this.contextModel = contextModel;
65
- this.configManagerContext = configManagerContext.create();
66
+ this.configManagerContext = this.#createManagerContext();
66
67
  this.tab = findActiveTab(this.tabs, tab);
67
68
  this.loadOrderConfigs.perform();
68
69
  }
69
70
 
71
+ /**
72
+ * Creates a contextual object for the order config manager.
73
+ */
74
+ #createManagerContext() {
75
+ const component = this;
76
+ const ctxmc = configManagerContext.create();
77
+ ctxmc.loadOrderConfigs = component.loadOrderConfigs;
78
+ ctxmc.createNewOrderConfig = component.createNewOrderConfig;
79
+ ctxmc.selectConfig = component.selectConfig;
80
+ ctxmc.unready = function () {
81
+ component.ready = false;
82
+ };
83
+ ctxmc.ready = function () {
84
+ component.ready = true;
85
+ };
86
+ ctxmc.childTaskStarted = function () {
87
+ component.childTaskRunning = true;
88
+ };
89
+ ctxmc.childTaskEnded = function () {
90
+ component.childTaskRunning = false;
91
+ };
92
+
93
+ return ctxmc;
94
+ }
95
+
70
96
  /**
71
97
  * Loads all available order configs asynchronously.
72
98
  *
@@ -81,20 +107,8 @@ export default class OrderConfigManagerComponent extends Component {
81
107
  this.configs = yield this.store.findAll('order-config').then(Array.from);
82
108
 
83
109
  let currentConfig;
84
- let initialOrderConfig = this.args.orderConfig;
85
110
  if (isArray(this.configs) && this.configs.length > 0) {
86
- if (initialOrderConfig) {
87
- currentConfig = this.configs.find((config) => {
88
- if (isModel(initialOrderConfig)) {
89
- return config.id === initialOrderConfig.id;
90
- }
91
-
92
- if (typeof initialOrderConfig === 'string') {
93
- return config.id === initialOrderConfig;
94
- }
95
- });
96
- }
97
-
111
+ currentConfig = this.configs.find((_) => _.key === 'transport');
98
112
  if (!currentConfig) {
99
113
  currentConfig = this.configs[0];
100
114
  }
@@ -208,7 +222,6 @@ export default class OrderConfigManagerComponent extends Component {
208
222
  * in 'contextComponentCallback'.
209
223
  */
210
224
  @action onConfigDeleting() {
211
- this.selectConfig(null);
212
225
  this.configManagerContext.trigger('onConfigDeleting');
213
226
  contextComponentCallback(this, 'onConfigDeleting', ...arguments);
214
227
  }
@@ -82,10 +82,11 @@
82
82
  <div class="flex flex-col items-center justify-center h-40">
83
83
  <h4 class="text-lg text-gray-400 mb-4">{{t "fleet-ops.component.order-config-manager.select-order-config-to-start"}}</h4>
84
84
  <Button
85
- @type="link"
85
+ @type="primary"
86
86
  @text={{t "fleet-ops.component.order-config-manager.new-order-config"}}
87
87
  @onClick={{Context.onClickCreateOrderConfig}}
88
88
  @permission="fleet-ops create order-config"
89
+ @icon="plus"
89
90
  class="text-blue-400"
90
91
  />
91
92
  </div>
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "keywords": [
6
6
  "fleetbase-extension",
package/extension.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "Fleet-Ops",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "repository": "https://github.com/fleetbase/fleetops",
6
6
  "license": "AGPL-3.0-or-later",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/fleetops-engine",
3
- "version": "0.6.15",
3
+ "version": "0.6.17",
4
4
  "description": "Fleet & Transport Management Extension for Fleetbase",
5
5
  "fleetbase": {
6
6
  "route": "fleet-ops"
@@ -0,0 +1,21 @@
1
+ <?php
2
+
3
+ use Illuminate\Database\Migrations\Migration;
4
+ use Illuminate\Database\Schema\Blueprint;
5
+ use Illuminate\Support\Facades\Schema;
6
+
7
+ return new class extends Migration {
8
+ public function up(): void
9
+ {
10
+ Schema::table('routes', function (Blueprint $table) {
11
+ $table->index(['company_uuid', 'id'], 'idx_routes_company_id');
12
+ });
13
+ }
14
+
15
+ public function down(): void
16
+ {
17
+ Schema::table('routes', function (Blueprint $table) {
18
+ $table->dropIndex('idx_routes_company_id');
19
+ });
20
+ }
21
+ };
@@ -30,6 +30,7 @@ use Fleetbase\Models\Company;
30
30
  use Fleetbase\Models\File;
31
31
  use Fleetbase\Models\Setting;
32
32
  use Fleetbase\Support\Auth;
33
+ use Fleetbase\Support\TemplateString;
33
34
  use Illuminate\Database\Eloquent\ModelNotFoundException;
34
35
  use Illuminate\Http\Request;
35
36
  use Illuminate\Http\UploadedFile;
@@ -461,6 +462,14 @@ class OrderController extends Controller
461
462
  ]);
462
463
  }
463
464
 
465
+ // vehicle assignment
466
+ if ($request->has('vehicle')) {
467
+ $input['vehicle_assigned_uuid'] = Utils::getUuid('vehicles', [
468
+ 'public_id' => $request->input('vehicle'),
469
+ 'company_uuid' => session('company'),
470
+ ]);
471
+ }
472
+
464
473
  // facilitator assignment
465
474
  if ($request->has('facilitator')) {
466
475
  $facilitator = Utils::getUuid(
@@ -1091,6 +1100,10 @@ class OrderController extends Controller
1091
1100
  $activity->set('pod_method', $order->pod_method);
1092
1101
  }
1093
1102
 
1103
+ // resolved status and details
1104
+ $activity->set('_resolved_status', TemplateString::resolve($activity->get('status', ''), $order));
1105
+ $activity->set('_resolved_details', TemplateString::resolve($activity->get('details', ''), $order));
1106
+
1094
1107
  return $activity;
1095
1108
  });
1096
1109
 
@@ -61,7 +61,7 @@ class OrderConfigController extends FleetOpsController
61
61
  }
62
62
 
63
63
  // `core_service` order configs cannot be deleted
64
- if ($orderConfig->core_service) {
64
+ if ($orderConfig->core_service === 1) {
65
65
  return response()->error('Core service order config\'s cannot be deleted.');
66
66
  }
67
67
 
@@ -72,5 +72,7 @@ class OrderConfigController extends FleetOpsController
72
72
 
73
73
  return new $this->resource($orderConfig);
74
74
  }
75
+
76
+ return response()->error('Unable to delete order config.');
75
77
  }
76
78
  }
@@ -32,6 +32,7 @@ use Fleetbase\Http\Requests\Internal\BulkDeleteRequest;
32
32
  use Fleetbase\Models\CustomFieldValue;
33
33
  use Fleetbase\Models\File;
34
34
  use Fleetbase\Models\Type;
35
+ use Fleetbase\Support\TemplateString;
35
36
  use Illuminate\Database\Eloquent\ModelNotFoundException;
36
37
  use Illuminate\Http\Request;
37
38
  use Illuminate\Support\Facades\DB;
@@ -722,20 +723,42 @@ class OrderController extends FleetOpsController
722
723
  *
723
724
  * @return \Illuminate\Http\Response
724
725
  */
725
- public function nextActivity(string $id)
726
+ public function nextActivity(string $id, Request $request)
726
727
  {
727
- $order = Order::withoutGlobalScopes()
728
- ->where('uuid', $id)
729
- ->orWhere('public_id', $id)
730
- ->first();
728
+ $waypointId = $request->input('waypoint');
731
729
 
732
- if (!$order) {
730
+ try {
731
+ $order = Order::findRecordOrFail($id, ['payload']);
732
+ } catch (ModelNotFoundException $exception) {
733
733
  return response()->error('No order found.');
734
734
  }
735
735
 
736
- $nextActivities = $order->config()->nextActivity();
736
+ // Get waypoint record if available
737
+ $waypoint = null;
738
+ if ($waypointId) {
739
+ $waypoint = Waypoint::where('payload_uuid', $order->payload_uuid)->whereHas('place', function ($query) use ($waypointId) {
740
+ $query->where('public_id', $waypointId);
741
+ })->first();
742
+ }
743
+
744
+ $activities = $order->config()->nextActivity($waypoint);
745
+
746
+ // If activity is to complete order add proof of delivery properties if required
747
+ // This is a temporary fix until activity is updated to handle POD on it's own
748
+ $activities = $activities->map(function ($activity) use ($order) {
749
+ if ($activity->completesOrder() && $order->pod_required) {
750
+ $activity->set('require_pod', true);
751
+ $activity->set('pod_method', $order->pod_method);
752
+ }
753
+
754
+ // resolved status and details
755
+ $activity->set('_resolved_status', TemplateString::resolve($activity->get('status', ''), $order));
756
+ $activity->set('_resolved_details', TemplateString::resolve($activity->get('details', ''), $order));
757
+
758
+ return $activity;
759
+ });
737
760
 
738
- return response()->json($nextActivities);
761
+ return response()->json($activities);
739
762
  }
740
763
 
741
764
  /**
@@ -7,6 +7,7 @@ use Fleetbase\Casts\Json;
7
7
  use Fleetbase\Casts\PolymorphicType;
8
8
  use Fleetbase\FleetOps\Support\Utils;
9
9
  use Fleetbase\FleetOps\Traits\HasTrackingNumber;
10
+ use Fleetbase\FleetOps\Traits\PayloadAccessors;
10
11
  use Fleetbase\Models\Model;
11
12
  use Fleetbase\Traits\HasApiModelBehavior;
12
13
  use Fleetbase\Traits\HasInternalId;
@@ -15,6 +16,9 @@ use Fleetbase\Traits\HasPublicId;
15
16
  use Fleetbase\Traits\HasUuid;
16
17
  use Fleetbase\Traits\SendsWebhooks;
17
18
  use Fleetbase\Traits\TracksApiCredential;
19
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
20
+ use Illuminate\Database\Eloquent\Relations\HasMany;
21
+ use Illuminate\Database\Eloquent\Relations\MorphTo;
18
22
  use Illuminate\Support\Carbon;
19
23
  use Illuminate\Support\Str;
20
24
  use Milon\Barcode\Facades\DNS2DFacade as DNS2D;
@@ -31,6 +35,7 @@ class Entity extends Model
31
35
  use HasTrackingNumber;
32
36
  use HasApiModelBehavior;
33
37
  use HasMetaAttributes;
38
+ use PayloadAccessors;
34
39
 
35
40
  /**
36
41
  * The database table used by the model.
@@ -162,81 +167,81 @@ class Entity extends Model
162
167
  }
163
168
 
164
169
  /**
165
- * @var \Illuminate\Database\Eloquent\Relations\BelongsTo
170
+ * @var BelongsTo
166
171
  */
167
- public function photo()
172
+ public function photo(): BelongsTo
168
173
  {
169
174
  return $this->belongsTo(\Fleetbase\Models\File::class);
170
175
  }
171
176
 
172
177
  /**
173
- * @var \Illuminate\Database\Eloquent\Relations\HasMany
178
+ * @var HasMany
174
179
  */
175
- public function files()
180
+ public function files(): HasMany
176
181
  {
177
182
  return $this->hasMany(\Fleetbase\Models\File::class);
178
183
  }
179
184
 
180
185
  /**
181
- * @var \Illuminate\Database\Eloquent\Relations\HasMany
186
+ * @var HasMany
182
187
  */
183
- public function proofs()
188
+ public function proofs(): HasMany
184
189
  {
185
190
  return $this->hasMany(Proof::class, 'subject_uuid');
186
191
  }
187
192
 
188
193
  /**
189
- * @var \Illuminate\Database\Eloquent\Relations\BelongsTo
194
+ * @var BelongsTo
190
195
  */
191
- public function destination()
196
+ public function destination(): BelongsTo
192
197
  {
193
198
  return $this->belongsTo(Place::class);
194
199
  }
195
200
 
196
201
  /**
197
- * @var \Illuminate\Database\Eloquent\Relations\BelongsTo
202
+ * @var BelongsTo
198
203
  */
199
- public function payload()
204
+ public function payload(): BelongsTo
200
205
  {
201
206
  return $this->belongsTo(Payload::class)->without(['entities']);
202
207
  }
203
208
 
204
209
  /**
205
- * @var \Illuminate\Database\Eloquent\Relations\BelongsTo
210
+ * @var BelongsTo
206
211
  */
207
- public function supplier()
212
+ public function supplier(): BelongsTo
208
213
  {
209
214
  return $this->belongsTo(Vendor::class, 'supplier_uuid', 'uuid');
210
215
  }
211
216
 
212
217
  /**
213
- * @var \Illuminate\Database\Eloquent\Relations\BelongsTo
218
+ * @var BelongsTo
214
219
  */
215
- public function driver()
220
+ public function driver(): BelongsTo
216
221
  {
217
222
  return $this->belongsTo(Driver::class, 'driver_assigned_uuid');
218
223
  }
219
224
 
220
225
  /**
221
- * @var \Illuminate\Database\Eloquent\Relations\BelongsTo
226
+ * @var BelongsTo
222
227
  */
223
- public function company()
228
+ public function company(): BelongsTo
224
229
  {
225
230
  return $this->belongsTo(\Fleetbase\Models\Company::class);
226
231
  }
227
232
 
228
233
  /**
229
- * @var \Illuminate\Database\Eloquent\Relations\BelongsTo
234
+ * @var BelongsTo
230
235
  */
231
- public function trackingNumber()
236
+ public function trackingNumber(): BelongsTo
232
237
  {
233
238
  return $this->belongsTo(TrackingNumber::class);
234
239
  }
235
240
 
236
241
  /**
237
- * @var \Illuminate\Database\Eloquent\Relations\MorphTo
242
+ * @var MorphTo
238
243
  */
239
- public function customer()
244
+ public function customer(): MorphTo
240
245
  {
241
246
  return $this->morphTo(__FUNCTION__, 'customer_type', 'customer_uuid')->withoutGlobalScopes();
242
247
  }
@@ -475,34 +480,4 @@ class Entity extends Model
475
480
  $this->customer_uuid = $model->uuid;
476
481
  $this->customer_type = Utils::getMutationType($model);
477
482
  }
478
-
479
- public function getPayload(): ?Payload
480
- {
481
- $this->load('payload');
482
-
483
- if ($this->payload instanceof Payload) {
484
- return $this->payload;
485
- }
486
-
487
- if (Str::isUuid($this->payload_uuid)) {
488
- return Payload::where('uuid', $this->payload_uuid)->first();
489
- }
490
-
491
- return null;
492
- }
493
-
494
- public function getTrashedPayload(): ?Payload
495
- {
496
- $payload = $this->payload()->withoutGlobalScopes()->first();
497
-
498
- if ($payload instanceof Payload) {
499
- return $payload;
500
- }
501
-
502
- if (Str::isUuid($this->payload_uuid)) {
503
- return Payload::where('uuid', $this->payload_uuid)->withoutGlobalScopes()->first();
504
- }
505
-
506
- return null;
507
- }
508
483
  }
@@ -6,6 +6,7 @@ use Barryvdh\DomPDF\Facade\Pdf;
6
6
  use Fleetbase\Casts\PolymorphicType;
7
7
  use Fleetbase\FleetOps\Support\Utils;
8
8
  use Fleetbase\FleetOps\Traits\HasTrackingNumber;
9
+ use Fleetbase\FleetOps\Traits\PayloadAccessors;
9
10
  use Fleetbase\Models\Model;
10
11
  use Fleetbase\Traits\HasPublicId;
11
12
  use Fleetbase\Traits\HasUuid;
@@ -21,6 +22,7 @@ class Waypoint extends Model
21
22
  use HasPublicId;
22
23
  use TracksApiCredential;
23
24
  use HasTrackingNumber;
25
+ use PayloadAccessors;
24
26
 
25
27
  /**
26
28
  * The database table used by the model.
@@ -3,13 +3,16 @@
3
3
  namespace Fleetbase\FleetOps\Traits;
4
4
 
5
5
  use Fleetbase\FleetOps\Flow\Activity;
6
+ use Fleetbase\FleetOps\Models\Order;
6
7
  use Fleetbase\FleetOps\Models\Proof;
7
8
  use Fleetbase\FleetOps\Models\TrackingNumber;
8
9
  use Fleetbase\FleetOps\Models\TrackingStatus;
9
10
  use Fleetbase\FleetOps\Support\Utils;
10
11
  use Fleetbase\LaravelMysqlSpatial\Types\Point;
12
+ use Fleetbase\Support\TemplateString;
11
13
  use Illuminate\Database\Eloquent\Model;
12
14
  use Illuminate\Support\Facades\DB;
15
+ use Illuminate\Support\Facades\Log;
13
16
 
14
17
  trait HasTrackingNumber
15
18
  {
@@ -55,11 +58,11 @@ trait HasTrackingNumber
55
58
  */
56
59
  public function createActivity(Activity $activity, $location = [], $proof = null): TrackingStatus
57
60
  {
58
- $status = $activity->get('status');
59
- $details = $activity->get('details');
60
- $code = $activity->get('code');
61
- $proof = static::resolveProof($proof);
62
- $activity = TrackingStatus::create([
61
+ $status = $this->resolveActivityTemplateString($activity->get('status', ''));
62
+ $details = $this->resolveActivityTemplateString($activity->get('details', ''));
63
+ $code = $activity->get('code');
64
+ $proof = static::resolveProof($proof);
65
+ $activity = TrackingStatus::create([
63
66
  'company_uuid' => data_get($this, 'company_uuid', session('company')),
64
67
  'tracking_number_uuid' => $this->tracking_number_uuid,
65
68
  'proof_uuid' => data_get($proof, 'uuid'),
@@ -89,11 +92,11 @@ trait HasTrackingNumber
89
92
  */
90
93
  public function insertActivity(Activity $activity, $location = [], $proof = null): string
91
94
  {
92
- $status = $activity->get('status');
93
- $details = $activity->get('details');
94
- $code = $activity->get('code');
95
- $proof = static::resolveProof($proof);
96
- $activityId = TrackingStatus::insertGetUuid([
95
+ $status = $this->resolveActivityTemplateString($activity->get('status', ''));
96
+ $details = $this->resolveActivityTemplateString($activity->get('details', ''));
97
+ $code = $activity->get('code');
98
+ $proof = static::resolveProof($proof);
99
+ $activityId = TrackingStatus::insertGetUuid([
97
100
  'company_uuid' => data_get($this, 'company_uuid', session('company')),
98
101
  'tracking_number_uuid' => $this->tracking_number_uuid,
99
102
  'proof_uuid' => data_get($proof, 'uuid'),
@@ -184,4 +187,55 @@ trait HasTrackingNumber
184
187
 
185
188
  return null;
186
189
  }
190
+
191
+ /**
192
+ * Resolve {placeholders} inside an activity template string.
193
+ *
194
+ * Behavior:
195
+ * - If called on an Order, resolves placeholders against that Order.
196
+ * - Otherwise, if the model exposes getOrder(), resolves against the returned Order.
197
+ * - If no suitable target model can be determined, returns the original template unchanged.
198
+ * - Uses App\Support\TemplateString::resolve() to process placeholders and modifiers
199
+ * (e.g., {waypoint.type}, {capitalize waypoint.type}, {order.number | snake | uppercase}).
200
+ *
201
+ * Robustness:
202
+ * - Fast path: if there are no braces, returns early.
203
+ * - Catches unexpected resolver failures and logs a warning rather than throwing inside UI flows.
204
+ *
205
+ * @param string $template the template containing {placeholders}
206
+ *
207
+ * @return string the resolved template string, or the original on fallback
208
+ */
209
+ private function resolveActivityTemplateString(string $template): string
210
+ {
211
+ // Fast path: no placeholders to resolve.
212
+ if ($template === '' || strpos($template, '{') === false) {
213
+ return $template;
214
+ }
215
+
216
+ // Determine which model should resolve dynamic properties.
217
+ // Prefer $this when it's already an Order; otherwise try a getOrder() accessor.
218
+ $target = $this instanceof Order
219
+ ? $this
220
+ : (method_exists($this, 'getOrder') ? $this->getOrder() : null);
221
+
222
+ // If we couldn't locate a suitable Eloquent model, leave template unchanged.
223
+ if (!$target instanceof Model) {
224
+ return $template;
225
+ }
226
+
227
+ try {
228
+ // Uses default resolver name 'resolveDynamicProperty' on the target model.
229
+ return TemplateString::resolve($template, $target, 'resolveDynamicProperty');
230
+ } catch (\Throwable $e) {
231
+ // Don't break user flows on template issues; log and return original.
232
+ Log::warning('Activity template resolution failed.', [
233
+ 'template' => $template,
234
+ 'target' => get_class($target),
235
+ 'message' => $e->getMessage(),
236
+ ]);
237
+
238
+ return $template;
239
+ }
240
+ }
187
241
  }
@@ -0,0 +1,126 @@
1
+ <?php
2
+
3
+ namespace Fleetbase\FleetOps\Traits;
4
+
5
+ use Fleetbase\FleetOps\Models\Order;
6
+ use Fleetbase\FleetOps\Models\Payload;
7
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
8
+ use Illuminate\Support\Str;
9
+
10
+ /**
11
+ * PayloadAccessors.
12
+ *
13
+ * Drop this trait into the model that has the `payload()` relationship and a `payload_uuid` FK.
14
+ * It provides:
15
+ * - getPayload(): returns the related Payload if present (with global scopes applied)
16
+ * - getTrashedPayload(): returns the related Payload without global scopes (e.g., soft-deleted)
17
+ * - getOrder(): returns the Order associated through Payload (or null)
18
+ *
19
+ * Design notes / best practices:
20
+ * - Avoids unconditional eager loading; checks relation cache first via relationLoaded().
21
+ * - Uses the relationship query as the primary source of truth, falling back to a direct lookup by UUID.
22
+ * - When resolving via direct lookup, caches the relation with setRelation() to prevent repeat queries.
23
+ * - Keeps the “ignore global scopes” behavior in a single private helper.
24
+ *
25
+ * Assumptions:
26
+ * - The host model defines `public function payload(): BelongsTo`.
27
+ * - `payload_uuid` stores a UUID, and `Payload`’s primary key is configured for UUIDs (so `find()` works).
28
+ */
29
+ trait PayloadAccessors
30
+ {
31
+ /**
32
+ * Get the associated Payload with global scopes applied.
33
+ *
34
+ * @return \App\Models\Payload|null
35
+ */
36
+ public function getPayload(): ?Payload
37
+ {
38
+ return $this->resolvePayload(ignoreGlobalScopes: false);
39
+ }
40
+
41
+ /**
42
+ * Get the associated Payload without global scopes (e.g., include soft-deleted).
43
+ *
44
+ * @return \App\Models\Payload|null
45
+ */
46
+ public function getTrashedPayload(): ?Payload
47
+ {
48
+ return $this->resolvePayload(ignoreGlobalScopes: true);
49
+ }
50
+
51
+ /**
52
+ * Convenience accessor: get the Order via the associated Payload.
53
+ *
54
+ * @return \App\Models\Order|null
55
+ */
56
+ public function getOrder(): ?Order
57
+ {
58
+ // Leverage PHP nullsafe operator; no extra branching needed.
59
+ return $this->getPayload()?->order;
60
+ }
61
+
62
+ /**
63
+ * Core resolver for the Payload relation.
64
+ *
65
+ * Strategy:
66
+ * 1) If the relation is already loaded and we are NOT ignoring global scopes, return it.
67
+ * 2) Otherwise, query via the relationship, optionally disabling global scopes.
68
+ * 3) If still not found and payload_uuid looks like a UUID, do a direct lookup on Payload,
69
+ * optionally disabling global scopes. If found, cache back into the relation.
70
+ *
71
+ * @param bool $ignoreGlobalScopes when true, fetch via `withoutGlobalScopes()`
72
+ *
73
+ * @return \App\Models\Payload|null
74
+ */
75
+ protected function resolvePayload(bool $ignoreGlobalScopes = false): ?Payload
76
+ {
77
+ // (1) If relation is already loaded and we accept scoped data, return it.
78
+ if (!$ignoreGlobalScopes && $this->relationLoaded('payload')) {
79
+ $related = $this->getRelation('payload');
80
+ if ($related instanceof Payload) {
81
+ return $related;
82
+ }
83
+ }
84
+
85
+ // (2) Query through the relationship (preferred, keeps FK semantics consistent).
86
+ $relation = $this->payload();
87
+ $query = $ignoreGlobalScopes ? $relation->withoutGlobalScopes() : $relation;
88
+
89
+ /** @var \App\Models\Payload|null $payload */
90
+ $payload = $query->first();
91
+
92
+ if ($payload instanceof Payload) {
93
+ // Cache the resolved relation for subsequent access on this model instance.
94
+ $this->setRelation('payload', $payload);
95
+
96
+ return $payload;
97
+ }
98
+
99
+ // (3) Fallback: direct lookup by UUID (useful if relation is not properly hydrated).
100
+ $uuid = $this->payload_uuid ?? null;
101
+ if ($uuid && Str::isUuid($uuid)) {
102
+ $payloadQuery = Payload::query();
103
+ if ($ignoreGlobalScopes) {
104
+ $payloadQuery->withoutGlobalScopes();
105
+ }
106
+
107
+ $payload = $payloadQuery->find($uuid); // relies on Payload PK being 'uuid'
108
+
109
+ if ($payload instanceof Payload) {
110
+ // Keep the relation cache consistent for this instance.
111
+ $this->setRelation('payload', $payload);
112
+
113
+ return $payload;
114
+ }
115
+ }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Relationship stub (documentational).
122
+ * Ensure your model actually defines this in the host model—not strictly required here,
123
+ * but included as a guide for expected signature.
124
+ */
125
+ abstract public function payload(): BelongsTo;
126
+ }
@@ -23,7 +23,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
23
23
  */
24
24
  $router->group(['prefix' => 'v1', 'middleware' => ['fleetbase.api', Fleetbase\FleetOps\Http\Middleware\TransformLocationMiddleware::class], 'namespace' => 'Api\v1'], function ($router) {
25
25
  // drivers routes
26
- $router->group(['prefix' => 'drivers', 'middleware' => [Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]], function () use ($router) {
26
+ $router->group(['prefix' => 'drivers', 'middleware' => []], function () use ($router) {
27
27
  $router->post('register-device', 'DriverController@registerDevice');
28
28
  $router->post('login-with-sms', 'DriverController@loginWithPhone');
29
29
  $router->post('verify-code', 'DriverController@verifyCode');
@@ -74,7 +74,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
74
74
  $router->delete('{id}', 'FuelReportController@delete');
75
75
  });
76
76
  // orders routes
77
- $router->group(['prefix' => 'orders', 'middleware' => [Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]], function () use ($router) {
77
+ $router->group(['prefix' => 'orders', 'middleware' => []], function () use ($router) {
78
78
  $router->post('/', 'OrderController@create');
79
79
  $router->get('/', 'OrderController@query');
80
80
  $router->get('{id}', 'OrderController@find');
@@ -251,8 +251,8 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
251
251
  $router->fleetbaseRoutes(
252
252
  'contacts',
253
253
  function ($router, $controller) {
254
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
255
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
254
+ $router->match(['get', 'post'], 'export', $controller('export'));
255
+ $router->post('import', $controller('import'));
256
256
  $router->get('facilitators/{id}', $controller('getAsFacilitator'));
257
257
  $router->get('customers/{id}', $controller('getAsCustomer'));
258
258
  $router->delete('bulk-delete', $controller('bulkDelete'));
@@ -261,11 +261,11 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
261
261
  $router->fleetbaseRoutes(
262
262
  'drivers',
263
263
  function ($router, $controller) {
264
- $router->get('statuses', $controller('statuses'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
265
- $router->get('avatars', $controller('avatars'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
266
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
264
+ $router->get('statuses', $controller('statuses'));
265
+ $router->get('avatars', $controller('avatars'));
266
+ $router->match(['get', 'post'], 'export', $controller('export'));
267
267
  $router->delete('bulk-delete', $controller('bulkDelete'));
268
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
268
+ $router->post('import', $controller('import'));
269
269
  }
270
270
  );
271
271
  $router->fleetbaseRoutes('entities');
@@ -276,31 +276,31 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
276
276
  $router->post('remove-driver', $controller('removeDriver'));
277
277
  $router->post('assign-vehicle', $controller('assignVehicle'));
278
278
  $router->post('remove-vehicle', $controller('removeVehicle'));
279
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
280
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
279
+ $router->match(['get', 'post'], 'export', $controller('export'));
280
+ $router->post('import', $controller('import'));
281
281
  $router->delete('bulk-delete', $controller('bulkDelete'));
282
282
  }
283
283
  );
284
284
  $router->fleetbaseRoutes(
285
285
  'fuel-reports',
286
286
  function ($router, $controller) {
287
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
288
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
287
+ $router->match(['get', 'post'], 'export', $controller('export'));
288
+ $router->post('import', $controller('import'));
289
289
  $router->delete('bulk-delete', $controller('bulkDelete'));
290
290
  }
291
291
  );
292
292
  $router->fleetbaseRoutes(
293
293
  'issues',
294
294
  function ($router, $controller) {
295
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
296
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
295
+ $router->match(['get', 'post'], 'export', $controller('export'));
296
+ $router->post('import', $controller('import'));
297
297
  $router->delete('bulk-delete', $controller('bulkDelete'));
298
298
  }
299
299
  );
300
300
  $router->fleetbaseRoutes(
301
301
  'integrated-vendors',
302
302
  function ($router, $controller) {
303
- $router->get('supported', $controller('getSupported'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
303
+ $router->get('supported', $controller('getSupported'));
304
304
  $router->delete('bulk-delete', $controller('bulkDelete'));
305
305
  }
306
306
  );
@@ -309,12 +309,12 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
309
309
  function ($router, $controller) {
310
310
  $router->get('default-config', $controller('getDefaultOrderConfig'));
311
311
  $router->get('search', $controller('search'));
312
- $router->get('statuses', $controller('statuses'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
312
+ $router->get('statuses', $controller('statuses'));
313
313
  $router->get('types', $controller('types'));
314
314
  $router->get('label/{id}', $controller('label'));
315
- $router->get('next-activity/{id}', $controller('nextActivity'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
316
- $router->get('{id}/tracker', 'OrderController@trackerInfo')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
317
- $router->get('{id}/eta', 'OrderController@waypointEtas')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
315
+ $router->get('next-activity/{id}', $controller('nextActivity'));
316
+ $router->get('{id}/tracker', 'OrderController@trackerInfo');
317
+ $router->get('{id}/eta', 'OrderController@waypointEtas');
318
318
  $router->post('process-imports', $controller('importFromFiles'));
319
319
  $router->patch('route/{id}', $controller('editOrderRoute'));
320
320
  $router->patch('update-activity/{id}', $controller('updateActivity'));
@@ -326,7 +326,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
326
326
  $router->patch('dispatch', $controller('dispatchOrder'));
327
327
  $router->patch('start', $controller('start'));
328
328
  $router->delete('bulk-delete', $controller('bulkDelete'));
329
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
329
+ $router->match(['get', 'post'], 'export', $controller('export'));
330
330
  }
331
331
  );
332
332
  $router->fleetbaseRoutes('order-configs');
@@ -334,12 +334,12 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
334
334
  $router->fleetbaseRoutes(
335
335
  'places',
336
336
  function ($router, $controller) {
337
- $router->get('search', $controller('search'))->middleware(['cache.headers:private;max_age=3600', Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
338
- $router->get('lookup', $controller('geocode'))->middleware(['cache.headers:private;max_age=3600', Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
337
+ $router->get('search', $controller('search'))->middleware(['cache.headers:private;max_age=3600']);
338
+ $router->get('lookup', $controller('geocode'))->middleware(['cache.headers:private;max_age=3600']);
339
339
  $router->get('avatars', $controller('avatars'));
340
340
  $router->match(['get', 'post'], 'export', $controller('export'));
341
341
  $router->delete('bulk-delete', $controller('bulkDelete'));
342
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
342
+ $router->post('import', $controller('import'));
343
343
  }
344
344
  );
345
345
  $router->fleetbaseRoutes('proofs');
@@ -348,7 +348,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
348
348
  $router->fleetbaseRoutes(
349
349
  'service-areas',
350
350
  function ($router, $controller) {
351
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
351
+ $router->match(['get', 'post'], 'export', $controller('export'));
352
352
  $router->delete('bulk-delete', $controller('bulkDelete'));
353
353
  }
354
354
  );
@@ -366,8 +366,8 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
366
366
  function ($router, $controller) {
367
367
  $router->delete('bulk-delete', $controller('bulkDelete'));
368
368
  $router->get('for-route', $controller('getServicesForRoute'));
369
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
370
- $router->get('for-route', $controller('getServicesForRoute'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
369
+ $router->match(['get', 'post'], 'export', $controller('export'));
370
+ $router->get('for-route', $controller('getServicesForRoute'));
371
371
  }
372
372
  );
373
373
  $router->fleetbaseRoutes('tracking-numbers');
@@ -375,10 +375,10 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
375
375
  $router->fleetbaseRoutes(
376
376
  'vehicles',
377
377
  function ($router, $controller) {
378
- $router->get('statuses', $controller('statuses'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
379
- $router->get('avatars', $controller('avatars'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
380
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
381
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
378
+ $router->get('statuses', $controller('statuses'));
379
+ $router->get('avatars', $controller('avatars'));
380
+ $router->match(['get', 'post'], 'export', $controller('export'));
381
+ $router->post('import', $controller('import'));
382
382
  $router->delete('bulk-delete', $controller('bulkDelete'));
383
383
  }
384
384
  );
@@ -386,14 +386,14 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
386
386
  $router->fleetbaseRoutes(
387
387
  'vendors',
388
388
  function ($router, $controller) {
389
- $router->get('statuses', $controller('statuses'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
390
- $router->match(['get', 'post'], 'export', $controller('export'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
389
+ $router->get('statuses', $controller('statuses'));
390
+ $router->match(['get', 'post'], 'export', $controller('export'));
391
391
  $router->get('facilitators/{id}', $controller('getAsFacilitator'));
392
392
  $router->get('customers/{id}', $controller('getAsCustomer'));
393
393
  $router->post('{id}/assign-driver', $controller('assignDriver'));
394
394
  $router->post('{id}/remove-driver', $controller('removeDriver'));
395
395
  $router->delete('bulk-delete', $controller('bulkDelete'));
396
- $router->post('import', $controller('import'))->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
396
+ $router->post('import', $controller('import'));
397
397
  }
398
398
  );
399
399
  $router->group(
@@ -417,7 +417,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
417
417
  }
418
418
  );
419
419
  $router->group(
420
- ['prefix' => 'geocoder', ['middleware' => [Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]]],
420
+ ['prefix' => 'geocoder', ['middleware' => []]],
421
421
  function ($router) {
422
422
  $router->get('reverse', 'GeocoderController@reverse');
423
423
  $router->get('query', 'GeocoderController@geocode');
@@ -427,12 +427,12 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
427
427
  ['prefix' => 'fleet-ops'],
428
428
  function ($router) {
429
429
  $router->group(
430
- ['prefix' => 'payments', ['middleware' => [Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]]],
430
+ ['prefix' => 'payments', ['middleware' => []]],
431
431
  function () use ($router) {
432
- $router->post('stripe-account', 'PaymentController@getStripeAccount')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
433
- $router->post('stripe-account-session', 'PaymentController@getStripeAccountSession')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
434
- $router->get('has-stripe-connect-account', 'PaymentController@hasStripeConnectAccount')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
435
- $router->get('payments-received', 'PaymentController@getCompanyReceivedPayments')->middleware([Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]);
432
+ $router->post('stripe-account', 'PaymentController@getStripeAccount');
433
+ $router->post('stripe-account-session', 'PaymentController@getStripeAccountSession');
434
+ $router->get('has-stripe-connect-account', 'PaymentController@hasStripeConnectAccount');
435
+ $router->get('payments-received', 'PaymentController@getCompanyReceivedPayments');
436
436
  }
437
437
  );
438
438
 
@@ -455,7 +455,7 @@ Route::prefix(config('fleetops.api.routing.prefix', null))->namespace('Fleetbase
455
455
  }
456
456
  );
457
457
  $router->group(
458
- ['prefix' => 'settings', 'middleware' => [Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class]],
458
+ ['prefix' => 'settings', 'middleware' => []],
459
459
  function ($router) {
460
460
  $router->get('customer-payments-config', 'SettingController@getCustomerPortalPaymentConfig');
461
461
  $router->post('customer-payments-config', 'SettingController@saveCustomerPortalPaymentConfig');