diff --git a/composer.json b/composer.json index 23aedb7a..994ab1b6 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/fleetops-api", - "version": "0.6.32", + "version": "0.6.33", "description": "Fleet & Transport Management Extension for Fleetbase", "keywords": [ "fleetbase-extension", diff --git a/extension.json b/extension.json index c58c0bfa..b481eae4 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "Fleet-Ops", - "version": "0.6.32", + "version": "0.6.33", "description": "Fleet & Transport Management Extension for Fleetbase", "repository": "https://github.com/fleetbase/fleetops", "license": "AGPL-3.0-or-later", diff --git a/package.json b/package.json index 241fe0d9..ceb31363 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/fleetops-engine", - "version": "0.6.32", + "version": "0.6.33", "description": "Fleet & Transport Management Extension for Fleetbase", "fleetbase": { "route": "fleet-ops" diff --git a/server/src/Console/Commands/TestEmail.php b/server/src/Console/Commands/TestEmail.php new file mode 100644 index 00000000..afe820ce --- /dev/null +++ b/server/src/Console/Commands/TestEmail.php @@ -0,0 +1,98 @@ +argument('email'); + $type = $this->option('type'); + + $this->info('Sending test email...'); + $this->info("Type: {$type}"); + $this->info("To: {$email}"); + + try { + switch ($type) { + case 'customer_credentials': + $this->sendCustomerCredentialsEmail($email); + break; + + default: + $this->error("Unknown email type: {$type}"); + return Command::FAILURE; + } + + $this->info('✓ Test email sent successfully!'); + return Command::SUCCESS; + } catch (\Exception $e) { + $this->error('Failed to send test email: ' . $e->getMessage()); + return Command::FAILURE; + } + } + + /** + * Send a test customer credentials email. + * + * @param string $email + * @return void + */ + private function sendCustomerCredentialsEmail(string $email): void + { + // Create a mock user + $user = new User([ + 'name' => 'Test Customer', + 'email' => $email, + ]); + + // Create a mock company + $company = new Company([ + 'name' => 'Test Company', + 'public_id' => 'test_company_123', + ]); + + // Create a mock customer + $customer = new Contact([ + 'name' => 'Test Customer', + 'email' => $email, + 'phone' => '+1234567890', + ]); + + // Set relations + $customer->setRelation('company', $company); + $customer->setRelation('user', $user); + + // Mock password + $plaintextPassword = 'TestPassword123!'; + + // Send the email + Mail::to($email)->send(new CustomerCredentialsMail($plaintextPassword, $customer)); + } +} diff --git a/server/src/Http/Controllers/Api/v1/DriverController.php b/server/src/Http/Controllers/Api/v1/DriverController.php index fe1654e5..c7c358e3 100644 --- a/server/src/Http/Controllers/Api/v1/DriverController.php +++ b/server/src/Http/Controllers/Api/v1/DriverController.php @@ -62,6 +62,9 @@ public function create(CreateDriverRequest $request) // Apply user infos $userDetails = User::applyUserInfoFromRequest($request, $userDetails); + // Set company_uuid before creating user + $userDetails['company_uuid'] = $company->uuid; + // create user account for driver $user = User::create($userDetails); diff --git a/server/src/Http/Controllers/Api/v1/VehicleController.php b/server/src/Http/Controllers/Api/v1/VehicleController.php index 87d52b48..2a1c244f 100644 --- a/server/src/Http/Controllers/Api/v1/VehicleController.php +++ b/server/src/Http/Controllers/Api/v1/VehicleController.php @@ -27,12 +27,10 @@ public function create(CreateVehicleRequest $request) { // get request input $input = $request->only(['status', 'make', 'model', 'year', 'trim', 'type', 'plate_number', 'vin', 'meta', 'online', 'location', 'altitude', 'heading', 'speed']); + // make sure company is set $input['company_uuid'] = session('company'); - // create instance of vehicle model - $vehicle = new Vehicle(); - // set default online if (!isset($input['online'])) { $input['online'] = 0; @@ -51,11 +49,8 @@ public function create(CreateVehicleRequest $request) $input['location'] = Utils::getPointFromCoordinates($request->only(['latitude', 'longitude'])); } - // apply user input to vehicle - $vehicle = $vehicle->fill($input); - - // save the vehicle - $vehicle->save(); + // create the vehicle (fires 'created' event for billing resource tracking) + $vehicle = Vehicle::create($input); // driver assignment if ($request->has('driver')) { diff --git a/server/src/Http/Controllers/Internal/v1/DriverController.php b/server/src/Http/Controllers/Internal/v1/DriverController.php index fccdab12..1daa9768 100644 --- a/server/src/Http/Controllers/Internal/v1/DriverController.php +++ b/server/src/Http/Controllers/Internal/v1/DriverController.php @@ -136,7 +136,37 @@ function (&$request, &$input) { if ($input->has('user_uuid')) { $user = User::where('uuid', $input->get('user_uuid'))->first(); - if ($user && $input->has('photo_uuid')) { + + // If user doesn't exist with provided UUID, create new user + if (!$user) { + $userInput = $input + ->only(['name', 'password', 'email', 'phone', 'status', 'avatar_uuid']) + ->filter() + ->toArray(); + + // handle `photo_uuid` + if (isset($input['photo_uuid']) && Str::isUuid($input['photo_uuid'])) { + $userInput['avatar_uuid'] = $input['photo_uuid']; + } + + // Make sure password is set + if (empty($userInput['password'])) { + $userInput['password'] = Str::random(14); + } + + // Set user company + $userInput['company_uuid'] = session('company', $company->uuid); + + // Apply user infos + $userInput = User::applyUserInfoFromRequest($request, $userInput); + + // Create user account + $user = User::create($userInput); + + // Set the user type to driver + $user->setType('driver'); + } elseif ($input->has('photo_uuid')) { + // Update existing user's avatar if photo provided $user->update(['avatar_uuid' => $input->get('photo_uuid')]); } } else { diff --git a/server/src/Http/Controllers/Internal/v1/OrderController.php b/server/src/Http/Controllers/Internal/v1/OrderController.php index 628611b0..63b4d214 100644 --- a/server/src/Http/Controllers/Internal/v1/OrderController.php +++ b/server/src/Http/Controllers/Internal/v1/OrderController.php @@ -29,14 +29,12 @@ use Fleetbase\FleetOps\Support\Utils; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\Internal\BulkActionRequest; -use Fleetbase\Http\Requests\Internal\BulkDeleteRequest; use Fleetbase\Models\File; use Fleetbase\Models\Type; use Fleetbase\Support\TemplateString; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\QueryException; use Illuminate\Http\Request; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Validator; @@ -330,36 +328,6 @@ public function importFromFiles(Request $request) ); } - /** - * Updates a order to canceled and updates order activity. - * - * @return \Illuminate\Http\Response - */ - public function bulkDelete(BulkDeleteRequest $request) - { - $ids = $request->input('ids', []); - - if (!$ids) { - return response()->error('Nothing to delete.'); - } - - /** @var Order */ - $count = Order::whereIn('uuid', $ids)->count(); - $deleted = Order::whereIn('uuid', $ids)->delete(); - - if (!$deleted) { - return response()->error('Failed to bulk delete orders.'); - } - - return response()->json( - [ - 'status' => 'OK', - 'message' => 'Deleted ' . $count . ' orders', - 'count' => $count, - ] - ); - } - /** * Updates a order to canceled and updates order activity. * diff --git a/server/src/Http/Controllers/Internal/v1/PlaceController.php b/server/src/Http/Controllers/Internal/v1/PlaceController.php index f52c0f01..4636eaae 100644 --- a/server/src/Http/Controllers/Internal/v1/PlaceController.php +++ b/server/src/Http/Controllers/Internal/v1/PlaceController.php @@ -10,7 +10,6 @@ use Fleetbase\FleetOps\Support\Geocoding; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; -use Fleetbase\Http\Requests\Internal\BulkDeleteRequest; use Fleetbase\LaravelMysqlSpatial\Types\Point; use Illuminate\Http\Request; use Illuminate\Support\Str; @@ -150,38 +149,6 @@ public function export(ExportRequest $request) return Excel::download(new PlaceExport($selections), $fileName); } - /** - * Bulk deletes resources. - * - * @return \Illuminate\Http\Response - */ - public function bulkDelete(BulkDeleteRequest $request) - { - $ids = $request->input('ids', []); - - if (!$ids) { - return response()->error('Nothing to delete.'); - } - - /** - * @var \Fleetbase\Models\Place - */ - $count = Place::whereIn('uuid', $ids)->applyDirectivesForPermissions('fleet-ops list place')->count(); - $deleted = Place::whereIn('uuid', $ids)->applyDirectivesForPermissions('fleet-ops list place')->delete(); - - if (!$deleted) { - return response()->error('Failed to bulk delete places.'); - } - - return response()->json( - [ - 'status' => 'OK', - 'message' => 'Deleted ' . $count . ' places', - ], - 200 - ); - } - /** * Get all avatar options for an vehicle. * diff --git a/server/src/Http/Controllers/Internal/v1/VendorController.php b/server/src/Http/Controllers/Internal/v1/VendorController.php index 56d64def..57d1e9e1 100644 --- a/server/src/Http/Controllers/Internal/v1/VendorController.php +++ b/server/src/Http/Controllers/Internal/v1/VendorController.php @@ -9,7 +9,6 @@ use Fleetbase\FleetOps\Models\Vendor; use Fleetbase\Http\Requests\ExportRequest; use Fleetbase\Http\Requests\ImportRequest; -use Fleetbase\Http\Requests\Internal\BulkDeleteRequest; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -85,36 +84,6 @@ public static function export(ExportRequest $request) return Excel::download(new VendorExport($selections), $fileName); } - /** - * Bulk delete resources. - * - * @return \Illuminate\Http\Response - */ - public function bulkDelete(BulkDeleteRequest $request) - { - $ids = $request->input('ids', []); - - if (!$ids) { - return response()->error('Nothing to delete.'); - } - - /** @var \Fleetbase\Models\Vendor */ - $count = Vendor::whereIn('uuid', $ids)->count(); - $deleted = Vendor::whereIn('uuid', $ids)->delete(); - - if (!$deleted) { - return response()->error('Failed to bulk delete vendors.'); - } - - return response()->json( - [ - 'status' => 'OK', - 'message' => 'Deleted ' . $count . ' vendors', - ], - 200 - ); - } - /** * Get all status options for an vehicle. * diff --git a/server/src/Http/Requests/CreateServiceRateRequest.php b/server/src/Http/Requests/CreateServiceRateRequest.php index b7b9d2b0..9f5a28b0 100644 --- a/server/src/Http/Requests/CreateServiceRateRequest.php +++ b/server/src/Http/Requests/CreateServiceRateRequest.php @@ -36,8 +36,8 @@ public function rules() 'base_fee' => ['numeric'], 'per_meter_unit' => ['required_if:rate_calculation_method,per_meter', 'string', 'in:km,m'], 'per_meter_flat_rate_fee' => ['required_if:rate_calculation_method,per_meter', 'numeric'], - 'meter_fees' => [Rule::requiredIf(function ($input) { - return in_array($input->rate_calculation_method, ['fixed_meter', 'fixed_rate']); + 'meter_fees' => [Rule::requiredIf(function () { + return in_array($this->input('rate_calculation_method'), ['fixed_meter', 'fixed_rate']); }), 'array'], 'meter_fees.*.distance' => ['numeric'], 'meter_fees.*.fee' => ['numeric'], diff --git a/server/src/Http/Requests/Internal/CreateDriverRequest.php b/server/src/Http/Requests/Internal/CreateDriverRequest.php index eac5a18c..08c4a48d 100644 --- a/server/src/Http/Requests/Internal/CreateDriverRequest.php +++ b/server/src/Http/Requests/Internal/CreateDriverRequest.php @@ -3,7 +3,9 @@ namespace Fleetbase\FleetOps\Http\Requests\Internal; use Fleetbase\FleetOps\Http\Requests\CreateDriverRequest as CreateDriverApiRequest; +use Fleetbase\FleetOps\Rules\ResolvablePoint; use Fleetbase\Support\Auth; +use Illuminate\Validation\Rule; class CreateDriverRequest extends CreateDriverApiRequest { @@ -25,13 +27,75 @@ public function authorize() public function rules() { $isCreating = $this->isMethod('POST'); + $isCreatingWithUser = $this->filled('driver.user_uuid'); + $shouldValidateUserAttributes = $isCreating && !$isCreatingWithUser; return [ - 'password' => 'nullable|string', - 'country' => 'nullable|size:2', - 'city' => 'nullable|string', - 'status' => 'nullable|string|in:active,inactive', - 'job' => 'nullable|exists:orders,public_id', + // Required fields for driver creation + 'name' => [Rule::requiredIf($shouldValidateUserAttributes), 'nullable', 'string', 'max:255'], + 'email' => [ + Rule::requiredIf($shouldValidateUserAttributes), + Rule::when($this->filled('email'), ['email']), + Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]) + ], + 'phone' => [ + Rule::requiredIf($shouldValidateUserAttributes), + Rule::when($shouldValidateUserAttributes, [Rule::unique('users')->whereNull('deleted_at')]) + ], + + // Optional fields + 'password' => 'nullable|string|min:8', + 'drivers_license_number' => 'nullable|string|max:255', + 'internal_id' => 'nullable|string|max:255', + 'country' => 'nullable|string|size:2', + 'city' => 'nullable|string|max:255', + 'vehicle' => 'nullable|string|starts_with:vehicle_|exists:vehicles,public_id', + 'status' => 'nullable|string|in:active,inactive', + 'vendor' => 'nullable|exists:vendors,public_id', + 'job' => 'nullable|exists:orders,public_id', + 'location' => ['nullable', new ResolvablePoint()], + 'latitude' => ['nullable', 'required_with:longitude', 'numeric'], + 'longitude' => ['nullable', 'required_with:latitude', 'numeric'], + + // Photo/avatar + 'photo_uuid' => 'nullable|exists:files,uuid', + 'avatar_uuid' => 'nullable|exists:files,uuid', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes() + { + return [ + 'name' => 'driver name', + 'email' => 'email address', + 'phone' => 'phone number', + 'drivers_license_number' => 'driver\'s license number', + 'internal_id' => 'internal ID', + 'photo_uuid' => 'photo', + 'avatar_uuid' => 'avatar', + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages() + { + return [ + 'name.required' => 'Driver name is required.', + 'email.required' => 'Email address is required.', + 'email.email' => 'Please provide a valid email address.', + 'email.unique' => 'This email address is already registered.', + 'phone.required' => 'Phone number is required.', + 'phone.unique' => 'This phone number is already registered.', + 'password.min' => 'Password must be at least 8 characters.', ]; } } diff --git a/server/src/Models/Place.php b/server/src/Models/Place.php index 8c4c01c4..a3f24d2c 100644 --- a/server/src/Models/Place.php +++ b/server/src/Models/Place.php @@ -333,6 +333,28 @@ public static function createFromGoogleAddress(\Geocoder\Provider\GoogleMaps\Mod { $instance = (new static())->fillWithGoogleAddress($address); + // Before saving or returning this instance check the database for a duplicate address + // it cannot have any owner, and must belong to this session + if ($companyUuid = session('company')) { + $duplicate = static::where([ + 'company_uuid' => $companyUuid, + 'street1' => $instance->street1, + 'city' => $instance->city, + 'country' => $instance->country, + ]) + ->when( + $instance->postal_code !== null, + fn ($q) => $q->where('postal_code', $instance->postal_code), + fn ($q) => $q->whereNull('postal_code') + ) + ->whereNull('owner_uuid') + ->first(); + + if ($duplicate) { + return $duplicate; + } + } + if ($saveInstance) { $instance->save(); } diff --git a/server/src/Providers/FleetOpsServiceProvider.php b/server/src/Providers/FleetOpsServiceProvider.php index 67a858a6..452ab51e 100644 --- a/server/src/Providers/FleetOpsServiceProvider.php +++ b/server/src/Providers/FleetOpsServiceProvider.php @@ -60,6 +60,7 @@ class FleetOpsServiceProvider extends CoreServiceProvider \Fleetbase\FleetOps\Console\Commands\PurgeUnpurchasedServiceQuotes::class, \Fleetbase\FleetOps\Console\Commands\SendDriverNotification::class, \Fleetbase\FleetOps\Console\Commands\ReplayVehicleLocations::class, + \Fleetbase\FleetOps\Console\Commands\TestEmail::class, ]; /**