فهرست منبع

feat: codigo unico para favorito

Gustavo Zanatta 1 هفته پیش
والد
کامیت
29245eb2b6

+ 20 - 0
app/Exceptions/FavoriteProviderException.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace App\Exceptions;
+
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use RuntimeException;
+
+class FavoriteProviderException extends RuntimeException
+{
+    public function __construct(string $messageKey, private readonly int $status = 422)
+    {
+        parent::__construct(__("messages.{$messageKey}"));
+    }
+
+    public function render(Request $request): JsonResponse
+    {
+        return response()->json(['payload' => null, 'message' => $this->getMessage()], $this->status);
+    }
+}

+ 17 - 0
app/Http/Controllers/ClientFavoriteProviderController.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Controllers;
 
+use App\Http\Requests\ClientFavoriteProviderByCodeRequest;
 use App\Http\Requests\ClientFavoriteProviderRequest;
 use App\Http\Resources\ClientFavoriteProviderResource;
 use App\Services\ClientFavoriteProviderService;
@@ -64,6 +65,22 @@ class ClientFavoriteProviderController extends Controller
 
     //
 
+    public function storeByCode(ClientFavoriteProviderByCodeRequest $request): JsonResponse
+    {
+        $data = $request->validated();
+
+        $favorite = $this->service->createByCode(
+            clientId: (int) data_get($data, 'client_id'),
+            code:     (string) data_get($data, 'code'),
+        );
+
+        return $this->successResponse(
+            payload: $favorite,
+            message: __('messages.favorite_code_added'),
+            code:    201,
+        );
+    }
+
     public function getFavoritedProviders(int $clientId): JsonResponse
     {
         $providerIds = $this->service->getFavoritedProviderIds($clientId);

+ 46 - 0
app/Http/Requests/ClientFavoriteProviderByCodeRequest.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Http\Requests;
+
+use App\Models\Provider;
+use Illuminate\Foundation\Http\FormRequest;
+
+class ClientFavoriteProviderByCodeRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    protected function prepareForValidation(): void
+    {
+        $this->merge([
+            'code' => Provider::normalizeShareCode($this->input('code')),
+        ]);
+    }
+
+    public function rules(): array
+    {
+        return [
+            'client_id' => ['required', 'integer', 'exists:clients,id'],
+
+            'code' => [
+                'required',
+                'string',
+                'size:'.Provider::SHARE_CODE_LENGTH,
+                'regex:/^['.Provider::SHARE_CODE_ALPHABET.']+$/',
+            ],
+        ];
+    }
+
+    public function messages(): array
+    {
+        return [
+            'client_id.required' => __('requests.client_favorite_provider.client_required'),
+            'client_id.exists'   => __('requests.client_favorite_provider.client_not_found'),
+            'code.required'      => __('requests.client_favorite_provider.code_required'),
+            'code.size'          => __('requests.client_favorite_provider.code_invalid'),
+            'code.regex'         => __('requests.client_favorite_provider.code_invalid'),
+        ];
+    }
+}

+ 1 - 0
app/Http/Resources/ProviderResource.php

@@ -17,6 +17,7 @@ class ProviderResource extends JsonResource
             'document'                       => $this->document,
             'rg'                             => $this->rg,
             'user_id'                        => $this->user_id,
+            'share_code'                     => $this->share_code,
             'user'                           => $this->user,
             'average_rating'                 => $this->average_rating,
             'total_services'                 => $this->total_services,

+ 1 - 0
app/Http/Resources/UserResource.php

@@ -26,6 +26,7 @@ class UserResource extends JsonResource
       'language'    => $this->language,
       'type'        => $this->type,
       'provider_id' => $this->provider?->id,
+      'provider_share_code' => $this->provider?->share_code,
 
       'push_notifications_enabled' => $this->push_notifications_enabled,
 

+ 38 - 1
app/Models/Provider.php

@@ -39,6 +39,7 @@ use Illuminate\Support\Facades\Auth;
  * @property string|null $recipient_document
  * @property string|null $recipient_type
  * @property string|null $recipient_code
+ * @property string|null $share_code
  * @property string|null $recipient_payment_mode
  * @property array<array-key, mixed>|null $recipient_default_bank_account
  * @property array<array-key, mixed>|null $recipient_transfer_settings
@@ -106,7 +107,11 @@ class Provider extends Model
 
     protected $table = "providers";
 
-    protected $guarded = ["id", "recipient_code"];
+    public const SHARE_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
+
+    public const SHARE_CODE_LENGTH = 6;
+
+    protected $guarded = ["id", "recipient_code", "share_code"];
 
     /**
      * Get the attributes that should be cast.
@@ -134,6 +139,15 @@ class Provider extends Model
         ];
     }
 
+    protected static function booted(): void
+    {
+        static::creating(function (self $provider) {
+            if (empty($provider->share_code)) {
+                $provider->share_code = self::generateUniqueShareCode();
+            }
+        });
+    }
+
     public function user(): BelongsTo
     {
         return $this->belongsTo(User::class, 'user_id');
@@ -276,4 +290,27 @@ class Provider extends Model
             ->where('provider_bank_accounts.is_active', true)
             ->whereNull('provider_bank_accounts.deleted_at');
     }
+
+    public static function generateUniqueShareCode(): string
+    {
+        $alphabet = self::SHARE_CODE_ALPHABET;
+        $lastIndex = strlen($alphabet) - 1;
+
+        do {
+            $code = '';
+
+            for ($i = 0; $i < self::SHARE_CODE_LENGTH; $i++) {
+                $code .= $alphabet[random_int(0, $lastIndex)];
+            }
+
+            $exists = self::withTrashed()->where('share_code', $code)->exists();
+        } while ($exists);
+
+        return $code;
+    }
+
+    public static function normalizeShareCode(?string $code): string
+    {
+        return strtoupper(trim((string) $code));
+    }
 }

+ 65 - 0
app/Services/ClientFavoriteProviderService.php

@@ -2,10 +2,15 @@
 
 namespace App\Services;
 
+use App\Enums\ApprovalStatusEnum;
+use App\Exceptions\FavoriteProviderException;
 use App\Http\Resources\ClientFavoriteProviderResource;
 use App\Models\ClientFavoriteProvider;
+use App\Models\ClientProviderBlock;
 use App\Models\Provider;
+use App\Models\ProviderClientBlock;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\DB;
 
 class ClientFavoriteProviderService
 {
@@ -79,6 +84,66 @@ class ClientFavoriteProviderService
 
     //
 
+    public function createByCode(int $clientId, string $code): ClientFavoriteProviderResource
+    {
+        $code = Provider::normalizeShareCode($code);
+
+        $provider = Provider::query()
+            ->where('share_code', $code)
+            ->where('approval_status', ApprovalStatusEnum::ACCEPTED->value)
+            ->visibleToCustomers()
+            ->with(['user', 'profileMedia'])
+            ->first();
+
+        if (! $provider) {
+            throw new FavoriteProviderException('favorite_code_not_found', 404);
+        }
+
+        $isBlocked = ProviderClientBlock::where('provider_id', $provider->id)
+            ->where('client_id', $clientId)
+            ->exists()
+            || ClientProviderBlock::where('client_id', $clientId)
+                ->where('provider_id', $provider->id)
+                ->exists();
+
+        if ($isBlocked) {
+            throw new FavoriteProviderException('favorite_code_blocked');
+        }
+
+        $favorite = DB::transaction(function () use ($clientId, $provider) {
+            $alreadyFavorite = ClientFavoriteProvider::where('client_id', $clientId)
+                ->where('provider_id', $provider->id)
+                ->exists();
+
+            if ($alreadyFavorite) {
+                throw new FavoriteProviderException('favorite_code_already_favorite');
+            }
+
+            $removed = ClientFavoriteProvider::onlyTrashed()
+                ->where('client_id', $clientId)
+                ->where('provider_id', $provider->id)
+                ->latest('id')
+                ->first();
+
+            if ($removed) {
+                $removed->restore();
+
+                return $removed;
+            }
+
+            return ClientFavoriteProvider::create([
+                'client_id'   => $clientId,
+                'provider_id' => $provider->id,
+            ]);
+        });
+
+        $favorite->setRelation('provider', $provider);
+
+        return new ClientFavoriteProviderResource($favorite);
+    }
+
+    //
+
     public function getFavoritedProviderIds(int $clientId): array
     {
         return ClientFavoriteProvider::where('client_id', $clientId)

+ 42 - 0
database/migrations/2026_08_31_120000_add_share_code_to_providers_table.php

@@ -0,0 +1,42 @@
+<?php
+
+use App\Models\Provider;
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->string('share_code', 6)->nullable()->after('recipient_code');
+
+            $table->unique('share_code');
+        });
+
+        Provider::withTrashed()
+            ->whereNull('share_code')
+            ->orderBy('id')
+            ->chunkById(200, function ($providers) {
+                foreach ($providers as $provider) {
+                    $provider->forceFill(['share_code' => Provider::generateUniqueShareCode()])->saveQuietly();
+                }
+            });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::table('providers', function (Blueprint $table) {
+            $table->dropUnique(['share_code']);
+
+            $table->dropColumn('share_code');
+        });
+    }
+};

+ 4 - 0
lang/en/messages.php

@@ -83,4 +83,8 @@ return [
     'service_package_schedule_already_assigned'     => 'The appointment already belongs to another service package.',
     'service_package_schedule_has_active_payment'   => 'The appointment already has a payment and cannot be added to the service package.',
     'gender_only_for_providers'                     => 'Gender can only be changed for provider users.',
+    'favorite_code_added'                            => 'Cleaner added to your favorites!',
+    'favorite_code_not_found'                        => 'Code not found. Check the code and try again.',
+    'favorite_code_already_favorite'                 => 'This cleaner is already in your favorites.',
+    'favorite_code_blocked'                          => 'This cleaner cannot be added to your favorites.',
 ];

+ 2 - 0
lang/en/requests.php

@@ -25,6 +25,8 @@ return [
         'provider_not_found' => 'Provider not found.',
         'already_favorite'   => 'This provider is already a favorite.',
         'notes_max'          => 'The notes may not exceed 1000 characters.',
+        'code_required'      => 'The code is required.',
+        'code_invalid'       => 'Invalid code. The code has 6 characters.',
     ],
 
     'custom_schedule_available' => [

+ 4 - 0
lang/es/messages.php

@@ -83,4 +83,8 @@ return [
     'service_package_schedule_already_assigned'     => 'El servicio programado ya pertenece a otro paquete.',
     'service_package_schedule_has_active_payment'   => 'El servicio programado ya tiene un pago y no puede añadirse al paquete.',
     'gender_only_for_providers'                     => 'El género solo puede modificarse para usuarios prestadores.',
+    'favorite_code_added'                            => '¡Limpiador añadido a tus favoritos!',
+    'favorite_code_not_found'                        => 'Código no encontrado. Verifica el código e inténtalo de nuevo.',
+    'favorite_code_already_favorite'                 => 'Este limpiador ya está en tus favoritos.',
+    'favorite_code_blocked'                          => 'No es posible añadir este limpiador a tus favoritos.',
 ];

+ 2 - 0
lang/es/requests.php

@@ -25,6 +25,8 @@ return [
         'provider_not_found' => 'Prestador no encontrado.',
         'already_favorite'   => 'Este prestador ya está en favoritos.',
         'notes_max'          => 'Las observaciones no pueden superar los 1000 caracteres.',
+        'code_required'      => 'El código es obligatorio.',
+        'code_invalid'       => 'Código inválido. El código tiene 6 caracteres.',
     ],
 
     'custom_schedule_available' => [

+ 4 - 0
lang/pt/messages.php

@@ -84,4 +84,8 @@ return [
     'service_package_schedule_has_active_payment'   => 'O agendamento já possui um pagamento e não pode ser adicionado ao pacote.',
     'gender_only_for_providers'                     => 'O gênero só pode ser alterado para usuários prestadores.',
     'schedule_belongs_to_package_use_package_endpoint' => 'Este agendamento pertence a um pacote de serviços. Use o endpoint de pacotes para aceitá-lo ou recusá-lo.',
+    'favorite_code_added'                            => 'Diarista adicionado aos seus favoritos!',
+    'favorite_code_not_found'                        => 'Código não encontrado. Confira o código e tente novamente.',
+    'favorite_code_already_favorite'                 => 'Este diarista já está nos seus favoritos.',
+    'favorite_code_blocked'                          => 'Não é possível favoritar este diarista.',
 ];

+ 2 - 0
lang/pt/requests.php

@@ -25,6 +25,8 @@ return [
         'provider_not_found' => 'Prestador não encontrado.',
         'already_favorite'   => 'Este prestador já está favoritado.',
         'notes_max'          => 'As observações não podem exceder 1000 caracteres.',
+        'code_required'      => 'O código é obrigatório.',
+        'code_invalid'       => 'Código inválido. O código tem 6 caracteres.',
     ],
 
     'custom_schedule_available' => [

+ 1 - 0
routes/authRoutes/client_favorite_provider.php

@@ -7,5 +7,6 @@ Route::get('/client/favorite-providers/{clientId}',  [ClientFavoriteProviderCont
 Route::get('/client/favorited-providers/{clientId}', [ClientFavoriteProviderController::class, 'getFavoritedProviders'])->middleware('permission:config.client_favorite_provider,view');
 Route::get('/client/favorite-provider/{id}',         [ClientFavoriteProviderController::class, 'show'])->middleware('permission:config.client_favorite_provider,view');
 Route::post('/client/favorite-provider',             [ClientFavoriteProviderController::class, 'store'])->middleware('permission:config.client_favorite_provider,add');
+Route::post('/client/favorite-provider/by-code',     [ClientFavoriteProviderController::class, 'storeByCode'])->middleware(['permission:config.client_favorite_provider,add', 'throttle:10,1']);
 Route::put('/client/favorite-provider/{id}',         [ClientFavoriteProviderController::class, 'update'])->middleware('permission:config.client_favorite_provider,edit');
 Route::delete('/client/favorite-provider/{id}',      [ClientFavoriteProviderController::class, 'destroy'])->middleware('permission:config.client_favorite_provider,delete');