feat(phone): split phone number components into separate fields and update related models and resources

This commit is contained in:
2026-09-30 13:39:24 -03:00
parent 77dd0e2715
commit f3e226130f
6 changed files with 201 additions and 2 deletions

View File

@@ -11,6 +11,9 @@ class AdminAppContextService
{ {
$tenant = $user->tenant() $tenant = $user->tenant()
->with([ ->with([
'socialMedia',
'addresses',
'phoneNumbers',
'menues' => fn ($query) => $query 'menues' => fn ($query) => $query
->whereHas( ->whereHas(
'roles', 'roles',

View File

@@ -8,7 +8,27 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class PhoneNumber extends Model class PhoneNumber extends Model
{ {
protected $fillable = ['label', 'number']; protected $attributes = ['country_code' => '+54', 'mobile_prefix' => '9'];
protected $fillable = ['label', 'country_code', 'mobile_prefix', 'area_code', 'local_number'];
public function getNumberAttribute(?string $legacyNumber): string
{
if ($this->local_number === null) {
return $legacyNumber ?? '';
}
$areaCode = $this->mobile_prefix && $this->area_code
? '('.$this->area_code.')'
: $this->area_code;
return implode(' ', array_filter([
$this->country_code,
$this->mobile_prefix,
$areaCode,
$this->local_number,
], fn ($part): bool => $part !== null && $part !== ''));
}
public static function dialableNumber(string $number): string public static function dialableNumber(string $number): string
{ {
@@ -17,7 +37,18 @@ class PhoneNumber extends Model
public function telUrl(): string public function telUrl(): string
{ {
return 'tel:'.self::dialableNumber($this->number); if ($this->country_code === null || $this->local_number === null) {
return 'tel:'.self::dialableNumber($this->number);
}
$areaCode = $this->area_code ?? '';
if ($this->country_code === '+54') {
$areaCode = ltrim($areaCode, '0');
}
return 'tel:'.self::dialableNumber(
$this->country_code.($this->mobile_prefix ?? '').$areaCode.$this->local_number
);
} }
public function tenants(): BelongsToMany public function tenants(): BelongsToMany

View File

@@ -12,6 +12,10 @@ class PhoneNumberResource extends JsonResource
return [ return [
'id' => $this->id, 'id' => $this->id,
'label' => $this->label, 'label' => $this->label,
'country_code' => $this->country_code,
'mobile_prefix' => $this->mobile_prefix,
'area_code' => $this->area_code,
'local_number' => $this->local_number,
'number' => $this->number, 'number' => $this->number,
'tel_url' => $this->resource->telUrl(), 'tel_url' => $this->resource->telUrl(),
'is_main' => $this->whenPivotLoaded('tenant_phone_numbers', fn () => (bool) $this->pivot->is_main), 'is_main' => $this->whenPivotLoaded('tenant_phone_numbers', fn () => (bool) $this->pivot->is_main),

View File

@@ -0,0 +1,94 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('phone_numbers', function (Blueprint $table): void {
$table->string('country_code', 8)->nullable()->default('+54');
$table->string('mobile_prefix', 4)->nullable()->default('9');
$table->string('area_code', 8)->nullable();
$table->string('local_number')->nullable();
});
DB::table('phone_numbers')->orderBy('id')->chunkById(100, function ($numbers): void {
foreach ($numbers as $phone) {
DB::table('phone_numbers')->where('id', $phone->id)
->update($this->components($phone->number));
}
});
Schema::table('phone_numbers', function (Blueprint $table): void {
$table->dropColumn('number');
});
}
public function down(): void
{
Schema::table('phone_numbers', function (Blueprint $table): void {
$table->string('number')->nullable();
});
DB::table('phone_numbers')->orderBy('id')->chunkById(100, function ($numbers): void {
foreach ($numbers as $phone) {
$parts = array_filter([
$phone->country_code,
$phone->mobile_prefix,
$phone->area_code,
$phone->local_number,
], fn ($part): bool => $part !== null && $part !== '');
DB::table('phone_numbers')->where('id', $phone->id)
->update(['number' => implode(' ', $parts)]);
}
});
Schema::table('phone_numbers', function (Blueprint $table): void {
$table->dropColumn(['country_code', 'mobile_prefix', 'area_code', 'local_number']);
});
}
/** @return array{country_code: ?string, mobile_prefix: ?string, area_code: ?string, local_number: string} */
private function components(string $number): array
{
$number = trim($number);
if (preg_match('/^(\+?\d{1,3})\s+(?:(9)\s+)?\(?(\d{2,5})\)?[\s-]+([\d\s-]+)$/', $number, $match)) {
return [
'country_code' => '+'.ltrim($match[1], '+'),
'mobile_prefix' => ($match[2] ?? '') ?: null,
'area_code' => $match[3],
'local_number' => preg_replace('/\D/', '', $match[4]),
];
}
if (preg_match('/^\(?(0\d{2,5})\)?[\s-]+([\d\s-]+)$/', $number, $match)) {
return [
'country_code' => null,
'mobile_prefix' => null,
'area_code' => $match[1],
'local_number' => preg_replace('/\D/', '', $match[2]),
];
}
if (preg_match('/^\+54(9?)(\d+)$/', $number, $match)) {
return [
'country_code' => '+54',
'mobile_prefix' => $match[1] === '9' ? '9' : null,
'area_code' => null,
'local_number' => $match[2],
];
}
return [
'country_code' => null,
'mobile_prefix' => null,
'area_code' => null,
'local_number' => $number,
];
}
};

View File

@@ -2,10 +2,13 @@
namespace Tests\Feature\Auth; namespace Tests\Feature\Auth;
use App\Domains\Core\Address\Models\Address;
use App\Domains\Core\Auth\Models\User; use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode; use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Core\Authorization\Models\Role; use App\Domains\Core\Authorization\Models\Role;
use App\Domains\Core\Menu\Models\Menu; use App\Domains\Core\Menu\Models\Menu;
use App\Domains\Core\PhoneNumber\Models\PhoneNumber;
use App\Domains\Core\Tenant\Models\SocialMedia;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum; use Laravel\Sanctum\Sanctum;
@@ -46,6 +49,21 @@ class AdminAppMeControllerTest extends TestCase
$tenant->menues()->sync([$catalog->code, $staff->code, $storefront->code]); $tenant->menues()->sync([$catalog->code, $staff->code, $storefront->code]);
$otherTenant->menues()->sync([$staff->code]); $otherTenant->menues()->sync([$staff->code]);
$instagram = SocialMedia::query()->create([
'code' => 'instagram', 'name' => 'Instagram', 'icon' => 'fa-brands fa-instagram',
]);
$tenant->socialMedia()->attach($instagram->code, ['url' => 'https://instagram.com/acme', 'orden' => 0]);
$address = Address::query()->create(['label' => 'Principal', 'address_text' => 'San Martín 123']);
$tenant->addresses()->attach($address->id, ['is_main' => true]);
$phone = PhoneNumber::query()->create([
'label' => 'Principal',
'country_code' => '+54',
'mobile_prefix' => '9',
'area_code' => '0341',
'local_number' => '6658247',
]);
$tenant->phoneNumbers()->attach($phone->id, ['is_main' => true]);
$user = User::factory()->create([ $user = User::factory()->create([
'nombre_apellido' => 'Admin Acme', 'nombre_apellido' => 'Admin Acme',
'email' => 'admin@acme.test', 'email' => 'admin@acme.test',
@@ -59,6 +77,11 @@ class AdminAppMeControllerTest extends TestCase
->assertJsonPath('data.user.id', $user->id) ->assertJsonPath('data.user.id', $user->id)
->assertJsonPath('data.user.email', 'admin@acme.test') ->assertJsonPath('data.user.email', 'admin@acme.test')
->assertJsonPath('data.tenant.codigo', 'acme') ->assertJsonPath('data.tenant.codigo', 'acme')
->assertJsonPath('data.tenant.social_media.0.url', 'https://instagram.com/acme')
->assertJsonPath('data.tenant.addresses.0.address_text', 'San Martín 123')
->assertJsonPath('data.tenant.phone_numbers.0.number', '+54 9 (0341) 6658247')
->assertJsonPath('data.tenant.phone_numbers.0.area_code', '0341')
->assertJsonPath('data.tenant.phone_numbers.0.local_number', '6658247')
->assertJsonCount(1, 'data.tenant.menues') ->assertJsonCount(1, 'data.tenant.menues')
->assertJsonPath('data.tenant.menues.0.code', 'admin.catalog') ->assertJsonPath('data.tenant.menues.0.code', 'admin.catalog')
->assertJsonPath('data.tenant.menues.0.label', 'Catálogo') ->assertJsonPath('data.tenant.menues.0.label', 'Catálogo')

View File

@@ -2,6 +2,7 @@
namespace Tests\Feature\Migrations; namespace Tests\Feature\Migrations;
use App\Domains\Core\PhoneNumber\Models\PhoneNumber;
use App\Domains\Core\PhoneNumber\Resources\PhoneNumberResource; use App\Domains\Core\PhoneNumber\Resources\PhoneNumberResource;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
@@ -96,6 +97,49 @@ class ContactPhoneNumbersTest extends TestCase
]); ]);
} }
public function test_it_splits_mobile_and_landline_numbers_and_builds_dialable_urls(): void
{
DB::table('phone_numbers')->insert([
['label' => 'Móvil', 'number' => '+54 9 (0341) 6658247'],
['label' => 'Fijo', 'number' => '+54 341 111-1111'],
['label' => 'Local', 'number' => '0341 1234567'],
['label' => 'Móvil sin signo', 'number' => '54 9 (0341) 6658247'],
]);
$migration = require database_path('migrations/2026_09_30_000700_split_phone_number_components.php');
$migration->up();
[$mobile, $landline, $local, $mobileWithoutPlus] = PhoneNumber::query()->orderBy('id')->get()->all();
$this->assertSame('+54', $mobile->country_code);
$this->assertSame('9', $mobile->mobile_prefix);
$this->assertSame('0341', $mobile->area_code);
$this->assertSame('6658247', $mobile->local_number);
$this->assertSame('+54 9 (0341) 6658247', $mobile->number);
$this->assertSame('tel:+5493416658247', $mobile->telUrl());
$this->assertSame('+54 341 1111111', $landline->number);
$this->assertSame('tel:+543411111111', $landline->telUrl());
$this->assertSame('0341 1234567', $local->number);
$this->assertSame('tel:03411234567', $local->telUrl());
$this->assertSame('+54 9 (0341) 6658247', $mobileWithoutPlus->number);
$resource = PhoneNumberResource::make($mobile)->resolve();
$this->assertSame('9', $resource['mobile_prefix']);
$this->assertSame('0341', $resource['area_code']);
$this->assertSame('tel:+5493416658247', $resource['tel_url']);
$created = PhoneNumber::query()->create([
'label' => 'Nuevo',
'area_code' => '0341',
'local_number' => '7777777',
]);
$this->assertSame('+54', $created->fresh()->country_code);
$this->assertSame('9', $created->fresh()->mobile_prefix);
$this->assertSame('+54 9 (0341) 7777777', $created->number);
$this->assertSame('tel:+5493417777777', $created->telUrl());
$migration->down();
$this->assertDatabaseHas('phone_numbers', ['id' => $mobile->id, 'number' => '+54 9 0341 6658247']);
}
private function contact(string $tenant, mixed $phone): void private function contact(string $tenant, mixed $phone): void
{ {
DB::table('tenants_menues')->insert([ DB::table('tenants_menues')->insert([