Przeglądaj źródła

feat: add value object cnpj

Gustavo Mantovani 22 godzin temu
rodzic
commit
53fc066166

+ 24 - 0
app/Casts/CnpjCast.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Casts;
+
+use App\ValueObjects\Cnpj;
+use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
+use Illuminate\Database\Eloquent\Model;
+
+class CnpjCast implements CastsAttributes
+{
+    public function get(Model $model, string $key, mixed $value, array $attributes): ?Cnpj
+    {
+        return $value === null ? null : new Cnpj((string) $value);
+    }
+
+    public function set(Model $model, string $key, mixed $value, array $attributes): ?string
+    {
+        if ($value === null) {
+            return null;
+        }
+
+        return ($value instanceof Cnpj ? $value : new Cnpj((string) $value))->value();
+    }
+}

+ 2 - 2
app/Http/Requests/FranchiseeRequest.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Http\Requests;
 namespace App\Http\Requests;
 
 
+use App\ValueObjects\Cnpj;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Foundation\Http\FormRequest;
 
 
 class FranchiseeRequest extends FormRequest
 class FranchiseeRequest extends FormRequest
@@ -9,8 +10,7 @@ class FranchiseeRequest extends FormRequest
     public function rules(): array
     public function rules(): array
     {
     {
         $rules = [
         $rules = [
-            // Add your validation rules here
-            //'field' => 'sometimes|string|max:255',
+            'cnpj' => ['sometimes', 'nullable', 'string', 'max:18', Cnpj::rule()],
         ];
         ];
 
 
         // Different rules for creation
         // Different rules for creation

+ 11 - 1
app/Http/Requests/UnitRequest.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Http\Requests;
 namespace App\Http\Requests;
 
 
+use App\ValueObjects\Cnpj;
 use App\ValueObjects\Cpf;
 use App\ValueObjects\Cpf;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Foundation\Http\FormRequest;
 
 
@@ -15,7 +16,7 @@ public function rules(): array
             'name'               => 'sometimes|required|string|max:255',
             'name'               => 'sometimes|required|string|max:255',
             'fantasy_name'       => 'sometimes|nullable|string|max:255',
             'fantasy_name'       => 'sometimes|nullable|string|max:255',
             'social_reason'      => 'sometimes|required|string|max:255',
             'social_reason'      => 'sometimes|required|string|max:255',
-            'cnpj'               => 'sometimes|required|string|max:20',
+            'cnpj'               => ['sometimes', 'required', 'string', 'max:18', Cnpj::rule()],
             'state_registration' => 'sometimes|nullable|string|max:50',
             'state_registration' => 'sometimes|nullable|string|max:50',
             'name_responsible'   => 'sometimes|required|string|max:255',
             'name_responsible'   => 'sometimes|required|string|max:255',
             'street'             => 'sometimes|required|string|max:255',
             'street'             => 'sometimes|required|string|max:255',
@@ -72,6 +73,15 @@ public function rules(): array
             ];
             ];
 
 
             foreach ($required as $field) {
             foreach ($required as $field) {
+                if (is_array($rules[$field])) {
+                    $rules[$field] = array_values(array_filter(
+                        $rules[$field],
+                        static fn (mixed $rule): bool => $rule !== 'sometimes',
+                    ));
+
+                    continue;
+                }
+
                 $rules[$field] = str_replace('sometimes|required', 'required', $rules[$field]);
                 $rules[$field] = str_replace('sometimes|required', 'required', $rules[$field]);
             }
             }
 
 

+ 3 - 1
app/Models/Franchisee.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Models;
 namespace App\Models;
 
 
+use App\ValueObjects\Cnpj;
 use App\ValueObjects\Cpf;
 use App\ValueObjects\Cpf;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model;
@@ -13,7 +14,7 @@
  * @property string $name
  * @property string $name
  * @property \App\ValueObjects\Cpf|null $cpf
  * @property \App\ValueObjects\Cpf|null $cpf
  * @property string|null $rg
  * @property string|null $rg
- * @property string|null $cnpj
+ * @property \App\ValueObjects\Cnpj|null $cnpj
  * @property string|null $phone
  * @property string|null $phone
  * @property string|null $cellphone_number
  * @property string|null $cellphone_number
  * @property string $street
  * @property string $street
@@ -62,6 +63,7 @@ class Franchisee extends Model
     protected $guarded = ['id'];
     protected $guarded = ['id'];
 
 
     protected $casts = [
     protected $casts = [
+        'cnpj'       => Cnpj::class,
         'cpf'        => Cpf::class,
         'cpf'        => Cpf::class,
         'created_at' => 'datetime',
         'created_at' => 'datetime',
         'updated_at' => 'datetime',
         'updated_at' => 'datetime',

+ 3 - 1
app/Models/Unit.php

@@ -2,6 +2,7 @@
 
 
 namespace App\Models;
 namespace App\Models;
 
 
+use App\ValueObjects\Cnpj;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -14,7 +15,7 @@
  * @property int $id
  * @property int $id
  * @property string|null $fantasy_name
  * @property string|null $fantasy_name
  * @property string $social_reason
  * @property string $social_reason
- * @property string $cnpj
+ * @property \App\ValueObjects\Cnpj $cnpj
  * @property string $phone_number
  * @property string $phone_number
  * @property string|null $cell_number
  * @property string|null $cell_number
  * @property string $street
  * @property string $street
@@ -81,6 +82,7 @@ class Unit extends Model
     protected $guarded = ['id'];
     protected $guarded = ['id'];
 
 
     protected $casts = [
     protected $casts = [
+        'cnpj'       => Cnpj::class,
         'created_at' => 'datetime',
         'created_at' => 'datetime',
         'updated_at' => 'datetime',
         'updated_at' => 'datetime',
         'deleted_at' => 'datetime',
         'deleted_at' => 'datetime',

+ 13 - 13
app/Services/Integrations/Asaas/AsaasCustomerService.php

@@ -21,7 +21,7 @@ public function __construct(AsaasClient $client)
      */
      */
     public function ensureFranchiseeCustomer(Unit $unit): string
     public function ensureFranchiseeCustomer(Unit $unit): string
     {
     {
-        $cpfCnpj = preg_replace('/[^0-9]/', '', $unit->cnpj);
+        $cpfCnpj = $unit->cnpj->value();
 
 
         if (empty($cpfCnpj)) {
         if (empty($cpfCnpj)) {
             throw new Exception("Unidade {$unit->fantasy_name} não possui CNPJ para ser cobrada.");
             throw new Exception("Unidade {$unit->fantasy_name} não possui CNPJ para ser cobrada.");
@@ -36,13 +36,13 @@ public function ensureFranchiseeCustomer(Unit $unit): string
 
 
         // Se não existe, cria um novo
         // Se não existe, cria um novo
         $payload = [
         $payload = [
-            'name' => $unit->social_reason ?? $unit->fantasy_name ?? 'Franquia',
-            'cpfCnpj' => $cpfCnpj,
-            'email' => $unit->email,
-            'postalCode' => preg_replace('/[^0-9]/', '', $unit->postal_code),
-            'address' => $unit->street,
+            'name'          => $unit->social_reason ?? $unit->fantasy_name ?? 'Franquia',
+            'cpfCnpj'       => $cpfCnpj,
+            'email'         => $unit->email,
+            'postalCode'    => preg_replace('/[^0-9]/', '', $unit->postal_code),
+            'address'       => $unit->street,
             'addressNumber' => $unit->address_number ?? 'S/N',
             'addressNumber' => $unit->address_number ?? 'S/N',
-            'province' => $unit->neighborhood,
+            'province'      => $unit->neighborhood,
         ];
         ];
 
 
         $mobilePhone = preg_replace('/[^0-9]/', '', $unit->cell_number ?? $unit->phone_number ?? '');
         $mobilePhone = preg_replace('/[^0-9]/', '', $unit->cell_number ?? $unit->phone_number ?? '');
@@ -75,13 +75,13 @@ public function ensureStudentCustomer(Student $student): string
         }
         }
 
 
         $payload = [
         $payload = [
-            'name' => $student->payer_name ?? $student->name,
-            'cpfCnpj' => $cpfCnpj,
-            'email' => $student->email,
-            'postalCode' => preg_replace('/[^0-9]/', '', $student->postal_code ?? ''),
-            'address' => $student->street,
+            'name'          => $student->payer_name ?? $student->name,
+            'cpfCnpj'       => $cpfCnpj,
+            'email'         => $student->email,
+            'postalCode'    => preg_replace('/[^0-9]/', '', $student->postal_code ?? ''),
+            'address'       => $student->street,
             'addressNumber' => $student->address_number ?? 'S/N',
             'addressNumber' => $student->address_number ?? 'S/N',
-            'province' => $student->neighborhood,
+            'province'      => $student->neighborhood,
         ];
         ];
 
 
         $mobilePhone = preg_replace('/[^0-9]/', '', $student->phone ?? '');
         $mobilePhone = preg_replace('/[^0-9]/', '', $student->phone ?? '');

+ 101 - 0
app/ValueObjects/Cnpj.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace App\ValueObjects;
+
+use App\Casts\CnpjCast;
+use Closure;
+use Illuminate\Contracts\Database\Eloquent\Castable;
+use InvalidArgumentException;
+use JsonSerializable;
+use Stringable;
+
+final readonly class Cnpj implements Castable, JsonSerializable, Stringable
+{
+    private string $value;
+
+    public function __construct(string $value)
+    {
+        $normalized = preg_replace('/[^A-Z0-9]/i', '', $value);
+        $normalized = $normalized === null ? null : strtoupper($normalized);
+
+        if ($normalized === null || ! self::isValid($normalized)) {
+            throw new InvalidArgumentException(__('validation.cnpj'));
+        }
+
+        $this->value = $normalized;
+    }
+
+    public static function castUsing(array $arguments): string
+    {
+        return CnpjCast::class;
+    }
+
+    public static function rule(): Closure
+    {
+        return static function (string $attribute, mixed $value, Closure $fail): void {
+            if (! is_string($value)) {
+                $fail(__('validation.cnpj'));
+
+                return;
+            }
+
+            try {
+                new self($value);
+            } catch (InvalidArgumentException) {
+                $fail(__('validation.cnpj'));
+            }
+        };
+    }
+
+    public function value(): string
+    {
+        return $this->value;
+    }
+
+    public function formatted(): string
+    {
+        return preg_replace(
+            '/^(.{2})(.{3})(.{3})(.{4})(\d{2})$/',
+            '$1.$2.$3/$4-$5',
+            $this->value,
+        );
+    }
+
+    public function __toString(): string
+    {
+        return $this->value;
+    }
+
+    public function jsonSerialize(): string
+    {
+        return $this->value;
+    }
+
+    private static function isValid(string $value): bool
+    {
+        if (! preg_match('/^[A-Z0-9]{12}\d{2}$/', $value) || preg_match('/^([A-Z0-9])\1{13}$/', $value)) {
+            return false;
+        }
+
+        $base = substr($value, 0, 12);
+        $firstDigit = self::checkDigit($base);
+        $secondDigit = self::checkDigit($base.$firstDigit);
+
+        return hash_equals($base.$firstDigit.$secondDigit, $value);
+    }
+
+    private static function checkDigit(string $value): int
+    {
+        $sum = 0;
+        $weight = 2;
+
+        for ($index = strlen($value) - 1; $index >= 0; $index--) {
+            $sum += (ord($value[$index]) - 48) * $weight;
+            $weight = $weight === 9 ? 2 : $weight + 1;
+        }
+
+        $remainder = $sum % 11;
+
+        return $remainder < 2 ? 0 : 11 - $remainder;
+    }
+}

+ 6 - 4
database/seeders/DeveloperTestSeeder.php

@@ -10,6 +10,7 @@
 use App\Models\User;
 use App\Models\User;
 use App\Models\UserType;
 use App\Models\UserType;
 use App\Models\UserTypePermission;
 use App\Models\UserTypePermission;
+use App\ValueObjects\Cnpj;
 use Illuminate\Database\Seeder;
 use Illuminate\Database\Seeder;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Cache;
 
 
@@ -143,15 +144,16 @@ public function run(): void
             $city = $cities->get($unitData['city']) ?? $fallbackCity;
             $city = $cities->get($unitData['city']) ?? $fallbackCity;
 
 
             $attributes = array_merge($unitData['attributes'], [
             $attributes = array_merge($unitData['attributes'], [
+                'cnpj'     => (new Cnpj($unitData['attributes']['cnpj']))->value(),
                 'city_id'  => $city->id,
                 'city_id'  => $city->id,
                 'state_id' => $city->state_id,
                 'state_id' => $city->state_id,
             ]);
             ]);
 
 
-            $unit = Unit::withTrashed()->updateOrCreate(
-                ['cnpj' => $attributes['cnpj']],
-                $attributes,
-            );
+            $unit = Unit::withTrashed()
+                ->whereIn('cnpj', [$attributes['cnpj'], $unitData['attributes']['cnpj']])
+                ->first() ?? new Unit;
 
 
+            $unit->fill($attributes)->save();
             $unit->restore();
             $unit->restore();
 
 
             $userType = UserType::updateOrCreate(
             $userType = UserType::updateOrCreate(

+ 1 - 0
lang/en/validation.php

@@ -36,6 +36,7 @@
     'cannot_delete_related' => 'The :attribute field cannot be deleted because it has related records.',
     'cannot_delete_related' => 'The :attribute field cannot be deleted because it has related records.',
     'confirmed' => 'The :attribute field confirmation does not match.',
     'confirmed' => 'The :attribute field confirmation does not match.',
     'contains' => 'The :attribute field is missing a required value.',
     'contains' => 'The :attribute field is missing a required value.',
+    'cnpj' => 'The provided CNPJ is invalid.',
     'cpf' => 'The provided CPF is invalid.',
     'cpf' => 'The provided CPF is invalid.',
     'current_password' => 'The password is incorrect.',
     'current_password' => 'The password is incorrect.',
     'date' => 'The :attribute field must be a valid date.',
     'date' => 'The :attribute field must be a valid date.',

+ 1 - 0
lang/es/validation.php

@@ -36,6 +36,7 @@
     'cannot_delete_related' => 'No se puede eliminar el registro porque tiene registros relacionados.',
     'cannot_delete_related' => 'No se puede eliminar el registro porque tiene registros relacionados.',
     'confirmed' => 'La confirmación del campo :attribute no coincide.',
     'confirmed' => 'La confirmación del campo :attribute no coincide.',
     'contains' => 'El campo :attribute carece de un valor requerido.',
     'contains' => 'El campo :attribute carece de un valor requerido.',
+    'cnpj' => 'El CNPJ proporcionado no es válido.',
     'cpf' => 'El CPF proporcionado no es válido.',
     'cpf' => 'El CPF proporcionado no es válido.',
     'current_password' => 'La contraseña es incorrecta.',
     'current_password' => 'La contraseña es incorrecta.',
     'date' => 'El campo :attribute debe ser una fecha válida.',
     'date' => 'El campo :attribute debe ser una fecha válida.',

+ 1 - 0
lang/pt/validation.php

@@ -37,6 +37,7 @@
     'cannot_delete_related' => 'Não é possível excluir o :attribute porque existem registros relacionados.',
     'cannot_delete_related' => 'Não é possível excluir o :attribute porque existem registros relacionados.',
     'confirmed' => 'A confirmação do campo :attribute não corresponde.',
     'confirmed' => 'A confirmação do campo :attribute não corresponde.',
     'contains' => 'O campo :attribute está faltando um valor obrigatório.',
     'contains' => 'O campo :attribute está faltando um valor obrigatório.',
+    'cnpj' => 'O CNPJ informado é inválido.',
     'cpf' => 'O CPF informado é inválido.',
     'cpf' => 'O CPF informado é inválido.',
     'current_password' => 'A senha está incorreta.',
     'current_password' => 'A senha está incorreta.',
     'date' => 'O campo :attribute deve ser uma data válida.',
     'date' => 'O campo :attribute deve ser uma data válida.',

+ 83 - 0
tests/Unit/ValueObjects/CnpjTest.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace Tests\Unit\ValueObjects;
+
+use App\Models\Franchisee;
+use App\Models\Unit;
+use App\ValueObjects\Cnpj;
+use Illuminate\Support\Facades\Validator;
+use InvalidArgumentException;
+use Tests\TestCase;
+
+class CnpjTest extends TestCase
+{
+    public function test_it_accepts_and_normalizes_a_numeric_cnpj(): void
+    {
+        $cnpj = new Cnpj('04.252.011/0001-10');
+
+        $this->assertSame('04252011000110', $cnpj->value());
+        $this->assertSame('04.252.011/0001-10', $cnpj->formatted());
+        $this->assertSame('04252011000110', (string) $cnpj);
+        $this->assertSame('"04252011000110"', json_encode($cnpj));
+    }
+
+    public function test_it_accepts_and_normalizes_an_alphanumeric_cnpj(): void
+    {
+        $cnpj = new Cnpj('12.abc.345/01de-35');
+
+        $this->assertSame('12ABC34501DE35', $cnpj->value());
+        $this->assertSame('12.ABC.345/01DE-35', $cnpj->formatted());
+    }
+
+    public function test_it_rejects_an_invalid_cnpj(): void
+    {
+        app()->setLocale('pt');
+
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage('O CNPJ informado é inválido.');
+
+        new Cnpj('04.252.011/0001-11');
+    }
+
+    public function test_its_request_rule_validates_cnpj(): void
+    {
+        $valid = Validator::make(
+            ['cnpj' => '12.ABC.345/01DE-35'],
+            ['cnpj' => ['required', Cnpj::rule()]],
+        );
+        $invalid = Validator::make(
+            ['cnpj' => '12.ABC.345/01DE-36'],
+            ['cnpj' => ['required', Cnpj::rule()]],
+        );
+
+        $this->assertTrue($valid->passes());
+        $this->assertTrue($invalid->fails());
+    }
+
+    public function test_its_request_rule_uses_the_current_locale(): void
+    {
+        app()->setLocale('es');
+
+        $validator = Validator::make(
+            ['cnpj' => '04.252.011/0001-11'],
+            ['cnpj' => [Cnpj::rule()]],
+        );
+
+        $this->assertSame(
+            'El CNPJ proporcionado no es válido.',
+            $validator->errors()->first('cnpj'),
+        );
+    }
+
+    public function test_models_cast_cnpj_to_the_value_object(): void
+    {
+        foreach ([Franchisee::class, Unit::class] as $modelClass) {
+            $model = new $modelClass;
+
+            $model->cnpj = '04.252.011/0001-10';
+
+            $this->assertInstanceOf(Cnpj::class, $model->cnpj);
+            $this->assertSame('04252011000110', $model->getAttributes()['cnpj']);
+        }
+    }
+}