@fleetbase/fleetops-engine 0.6.16 → 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.
package/composer.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetbase/fleetops-api",
3
- "version": "0.6.16",
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.16",
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.16",
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
 
@@ -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
+ }