Skip to content

Commit 81c086c

Browse files
authored
Merge pull request #206 from fleetbase/dev-v1.6.41
v1.6.41
2 parents e19c142 + 383b9c5 commit 81c086c

30 files changed

Lines changed: 519 additions & 206 deletions

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fleetbase/core-api",
3-
"version": "1.6.40",
3+
"version": "1.6.41",
44
"description": "Core Framework and Resources for Fleetbase API",
55
"keywords": [
66
"fleetbase",

migrations/2026_04_05_000001_add_scheduled_status_to_schedule_items_table.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
* 'pending' is retained for backwards compatibility (e.g. manually-created items
1414
* that have not yet been confirmed).
1515
*/
16-
return new class extends Migration
17-
{
16+
return new class extends Migration {
1817
public function up(): void
1918
{
2019
DB::statement("

migrations/2026_04_06_000002_add_company_uuid_to_schedule_items_table.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
use Illuminate\Database\Migrations\Migration;
44
use Illuminate\Database\Schema\Blueprint;
5-
use Illuminate\Support\Facades\Schema;
65
use Illuminate\Support\Facades\DB;
6+
use Illuminate\Support\Facades\Schema;
77

88
return new class extends Migration {
99
public function up(): void
@@ -13,13 +13,13 @@ public function up(): void
1313
});
1414

1515
// Backfill from the parent schedule
16-
DB::statement("
16+
DB::statement('
1717
UPDATE schedule_items si
1818
JOIN schedules s ON s.uuid = si.schedule_uuid
1919
SET si.company_uuid = s.company_uuid
2020
WHERE si.company_uuid IS NULL
2121
AND si.schedule_uuid IS NOT NULL
22-
");
22+
');
2323
}
2424

2525
public function down(): void
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
/**
9+
* Run the migrations.
10+
*
11+
* Adds a nullable JSON `meta` column to the `invites` table so that
12+
* arbitrary key-value data (e.g. role_uuid for user invitations) can be
13+
* stored on an invite record without requiring dedicated columns for each
14+
* use-case. The HasMetaAttributes trait is used to read and write values.
15+
*
16+
* @return void
17+
*/
18+
public function up()
19+
{
20+
Schema::table('invites', function (Blueprint $table) {
21+
$table->json('meta')->nullable()->after('reason');
22+
});
23+
}
24+
25+
/**
26+
* Reverse the migrations.
27+
*
28+
* @return void
29+
*/
30+
public function down()
31+
{
32+
Schema::table('invites', function (Blueprint $table) {
33+
$table->dropColumn('meta');
34+
});
35+
}
36+
};

src/Console/Commands/SyncSandbox.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@ public function handle()
5656
DB::connection('sandbox')
5757
->table('api_credentials')
5858
->truncate();
59+
DB::connection('sandbox')
60+
->table('company_users')
61+
->truncate();
5962
}
6063

6164
// Models that need to be synced from Production to Sandbox
62-
$syncable = [\Fleetbase\Models\User::class, \Fleetbase\Models\Company::class, \Fleetbase\Models\ApiCredential::class];
65+
$syncable = [\Fleetbase\Models\User::class, \Fleetbase\Models\Company::class, \Fleetbase\Models\CompanyUser::class, \Fleetbase\Models\ApiCredential::class];
6366

6467
// Sync each syncable data model
6568
foreach ($syncable as $model) {

src/Http/Controllers/Internal/v1/ApiCredentialController.php

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
use Fleetbase\Http\Controllers\FleetbaseController;
77
use Fleetbase\Http\Requests\ExportRequest;
88
use Fleetbase\Models\ApiCredential;
9+
use Fleetbase\Models\Company;
10+
use Fleetbase\Models\CompanyUser;
11+
use Fleetbase\Models\User;
912
use Illuminate\Http\Request;
13+
use Illuminate\Support\Carbon;
1014
use Illuminate\Support\Facades\Auth;
1115
use Illuminate\Support\Facades\DB;
16+
use Illuminate\Support\Facades\Schema;
1217
use Illuminate\Support\Str;
1318
use Maatwebsite\Excel\Facades\Excel;
1419

@@ -28,6 +33,124 @@ class ApiCredentialController extends FleetbaseController
2833
*/
2934
public $service = 'developers';
3035

36+
/**
37+
* Create a new API credential record.
38+
*
39+
* Overrides the generic createRecord to ensure that when a test/sandbox key
40+
* is being created, the current user and company are mirrored into the sandbox
41+
* database first. Without this, the foreign key constraint on `api_credentials.user_uuid`
42+
* (which references `users.uuid` in the sandbox DB) will fail because the user
43+
* and company rows only exist in the production database.
44+
*
45+
* @return \Illuminate\Http\Response
46+
*/
47+
public function createRecord(Request $request)
48+
{
49+
// Determine if this is a sandbox/test key creation request.
50+
$isSandbox = \Fleetbase\Support\Utils::isTrue($request->header('Access-Console-Sandbox'));
51+
52+
if ($isSandbox) {
53+
// Ensure the current user and company exist in the sandbox DB before
54+
// attempting the insert, so the FK constraints are satisfied.
55+
$this->syncCurrentSessionToSandbox($request);
56+
}
57+
58+
return parent::createRecord($request);
59+
}
60+
61+
/**
62+
* Mirrors the currently authenticated user, their company, and the company–user
63+
* membership record into the sandbox database.
64+
*
65+
* This is a targeted, on-demand version of the `sandbox:sync` Artisan command,
66+
* scoped only to the records needed to satisfy the foreign key constraints when
67+
* inserting a new test-mode `api_credentials` row.
68+
*/
69+
protected function syncCurrentSessionToSandbox(Request $request): void
70+
{
71+
$userUuid = session('user');
72+
$companyUuid = session('company');
73+
74+
if (!$userUuid || !$companyUuid) {
75+
return;
76+
}
77+
78+
// Temporarily disable FK checks so we can upsert in any order.
79+
Schema::connection('sandbox')->disableForeignKeyConstraints();
80+
81+
try {
82+
// --- Sync User ---
83+
$user = User::on('mysql')->withoutGlobalScopes()->where('uuid', $userUuid)->first();
84+
if ($user) {
85+
$this->upsertModelToSandbox($user);
86+
}
87+
88+
// --- Sync Company ---
89+
$company = Company::on('mysql')->withoutGlobalScopes()->where('uuid', $companyUuid)->first();
90+
if ($company) {
91+
$this->upsertModelToSandbox($company);
92+
}
93+
94+
// --- Sync CompanyUser pivot ---
95+
$companyUser = CompanyUser::on('mysql')
96+
->withoutGlobalScopes()
97+
->where('user_uuid', $userUuid)
98+
->where('company_uuid', $companyUuid)
99+
->first();
100+
if ($companyUser) {
101+
$this->upsertModelToSandbox($companyUser);
102+
}
103+
} finally {
104+
Schema::connection('sandbox')->enableForeignKeyConstraints();
105+
}
106+
}
107+
108+
/**
109+
* Upserts a single Eloquent model record into the sandbox database.
110+
*
111+
* Mirrors the approach used in the `sandbox:sync` Artisan command:
112+
* reduces the record to its fillable attributes, normalises datetime
113+
* fields to strings, JSON-encodes any Json-cast columns, then performs
114+
* an `updateOrInsert` keyed on `uuid`.
115+
*/
116+
protected function upsertModelToSandbox(\Illuminate\Database\Eloquent\Model $model): void
117+
{
118+
$clone = collect($model->toArray())
119+
->only($model->getFillable())
120+
->toArray();
121+
122+
if (!isset($clone['uuid']) || !is_string($clone['uuid'])) {
123+
return;
124+
}
125+
126+
// Normalise datetime columns to plain strings.
127+
foreach ($clone as $key => $value) {
128+
if (isset($clone[$key]) && Str::endsWith($key, '_at')) {
129+
try {
130+
$clone[$key] = Carbon::parse($clone[$key])->toDateTimeString();
131+
} catch (\Exception $e) {
132+
$clone[$key] = null;
133+
}
134+
}
135+
}
136+
137+
// JSON-encode any Json-cast columns that are still arrays/objects.
138+
$jsonColumns = collect($model->getCasts())
139+
->filter(fn ($cast) => Str::contains($cast, 'Json'))
140+
->keys()
141+
->toArray();
142+
143+
foreach ($clone as $key => $value) {
144+
if (in_array($key, $jsonColumns) && (is_object($value) || is_array($value))) {
145+
$clone[$key] = json_encode($value);
146+
}
147+
}
148+
149+
DB::connection('sandbox')
150+
->table($model->getTable())
151+
->updateOrInsert(['uuid' => $clone['uuid']], $clone);
152+
}
153+
31154
/**
32155
* Export the companies/users api credentials to excel or csv.
33156
*

src/Http/Controllers/Internal/v1/ScheduleExceptionController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ class ScheduleExceptionController extends FleetbaseController
1919

2020
/**
2121
* The ScheduleService instance.
22-
*
23-
* @var ScheduleService
2422
*/
2523
protected ScheduleService $scheduleService;
2624

src/Http/Controllers/Internal/v1/ScheduleTemplateController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ class ScheduleTemplateController extends FleetbaseController
2020

2121
/**
2222
* The ScheduleService instance.
23-
*
24-
* @var ScheduleService
2523
*/
2624
protected ScheduleService $scheduleService;
2725

0 commit comments

Comments
 (0)