233 lines
12 KiB
PHP
233 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Tenant;
|
|
|
|
use App\Domains\Core\Address\Models\Address;
|
|
use App\Domains\Core\Auth\Models\User;
|
|
use App\Domains\Core\Authorization\Enums\RoleCode;
|
|
use App\Domains\Core\Tenant\Models\SocialMedia;
|
|
use App\Domains\Core\Tenant\Models\Tenant;
|
|
use App\Domains\Core\Tenant\Resources\TenantResource;
|
|
use App\Domains\Core\Tenant\Services\ContactService;
|
|
use App\Domains\Ticketing\Event\Models\Event;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Laravel\Sanctum\Sanctum;
|
|
use Tests\TestCase;
|
|
|
|
class AdminAppContactControllerTest extends TestCase
|
|
{
|
|
private Tenant $tenant;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
// Use the real contact migrations in the guarded in-memory database. The
|
|
// unrelated historical migration chain cannot currently run on SQLite.
|
|
Schema::create('tenants', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->string('codigo')->unique();
|
|
$table->string('nombre');
|
|
$table->string('dominio');
|
|
foreach (array_keys((new Tenant)->getAttributes()) as $column) {
|
|
$table->string($column)->nullable();
|
|
}
|
|
$table->unsignedBigInteger('active_event_id')->nullable();
|
|
$table->timestamps();
|
|
});
|
|
foreach ([
|
|
'2026_07_23_000200_create_social_media_tables.php',
|
|
'2026_07_23_000300_add_orden_to_tenant_social_media_table.php',
|
|
'2026_09_30_000000_create_addresses_and_tenant_addresses_tables.php',
|
|
'2026_09_30_000400_create_phone_numbers_and_tenant_phone_numbers_tables.php',
|
|
'2026_09_30_000700_split_phone_number_components.php',
|
|
] as $migration) {
|
|
(require database_path('migrations/'.$migration))->up();
|
|
}
|
|
Schema::create('events', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->string('tenant_code');
|
|
$table->string('slug');
|
|
$table->string('title');
|
|
$table->unsignedBigInteger('address_id')->nullable();
|
|
$table->timestamps();
|
|
});
|
|
Schema::create('event_social_media', function (Blueprint $table): void {
|
|
$table->id();
|
|
$table->unsignedBigInteger('event_id');
|
|
$table->string('social_media_code');
|
|
$table->string('url');
|
|
$table->unsignedInteger('orden');
|
|
$table->timestamps();
|
|
});
|
|
$this->tenant = Tenant::create(['codigo' => 'contact-test', 'nombre' => 'Contact', 'dominio' => 'contact.test']);
|
|
SocialMedia::firstOrCreate(['code' => 'instagram'], ['name' => 'Instagram', 'icon' => 'instagram']);
|
|
}
|
|
|
|
private function login(): void
|
|
{
|
|
Sanctum::actingAs(new User([
|
|
'rol_codigo' => RoleCode::AdminApp->value,
|
|
'tenant_codigo' => $this->tenant->codigo,
|
|
]));
|
|
}
|
|
|
|
private function payload(): array
|
|
{
|
|
return [
|
|
'social_media' => [['code' => 'instagram', 'url' => 'https://instagram.com/contact']],
|
|
'addresses' => [['address_text' => 'San Martín 123']],
|
|
'phone_numbers' => [[
|
|
'country_code' => '+54', 'mobile_prefix' => null, 'area_code' => '341',
|
|
'local_number' => '5551234',
|
|
]],
|
|
];
|
|
}
|
|
|
|
public function test_authentication_and_admin_role_are_required(): void
|
|
{
|
|
$this->getJson('/api/v1/adminapp/tenant/contact')->assertUnauthorized();
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertUnauthorized();
|
|
Sanctum::actingAs(new User(['tenant_codigo' => $this->tenant->codigo]));
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertForbidden();
|
|
}
|
|
|
|
public function test_contact_is_persisted_and_exposed_in_storefront_resource(): void
|
|
{
|
|
$this->login();
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertOk()
|
|
->assertJsonPath('data.phone_numbers.0.mobile_prefix', null)
|
|
->assertJsonPath('data.addresses.0.is_main', true);
|
|
$this->getJson('/api/v1/adminapp/tenant/contact')->assertOk()
|
|
->assertJsonPath('data.addresses.0.address_text', 'San Martín 123');
|
|
$storefront = $this->storefrontData();
|
|
$this->assertSame('https://instagram.com/contact', $storefront['tenant_social_media'][0]['url']);
|
|
$this->assertSame('San Martín 123', $storefront['main_address']->resolve()['address_text']);
|
|
$this->assertSame('tel:+543415551234', $storefront['main_phone_number']->resolve()['tel_url']);
|
|
}
|
|
|
|
public function test_existing_contacts_can_be_updated_replaced_and_cleared(): void
|
|
{
|
|
$this->login();
|
|
$first = $this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertOk()->json('data');
|
|
$payload = $this->payload();
|
|
$payload['addresses'] = [
|
|
['id' => $first['addresses'][0]['id'], 'address_text' => 'Updated', 'is_main' => false],
|
|
['address_text' => 'New principal', 'is_main' => true],
|
|
];
|
|
$payload['phone_numbers'][0]['id'] = $first['phone_numbers'][0]['id'];
|
|
$payload['phone_numbers'][0]['local_number'] = '7771234';
|
|
$payload['phone_numbers'][0]['is_main'] = false;
|
|
$payload['phone_numbers'][] = [...$this->payload()['phone_numbers'][0], 'local_number' => '8881234', 'is_main' => true];
|
|
$updated = $this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertOk()->assertJsonCount(2, 'data.addresses')->json('data');
|
|
// Position wins, even if a client sends conflicting is_main flags.
|
|
$this->assertSame('Updated', $this->tenant->mainAddress()->firstOrFail()->address_text);
|
|
$this->assertSame('7771234', $this->tenant->mainPhoneNumber()->firstOrFail()->local_number);
|
|
$payload['addresses'] = array_reverse($updated['addresses']);
|
|
$payload['addresses'] = array_map(fn ($item) => array_intersect_key($item, array_flip(['id', 'address_text'])), $payload['addresses']);
|
|
$payload['phone_numbers'] = array_reverse($payload['phone_numbers']);
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertOk()
|
|
->assertJsonPath('data.addresses.0.address_text', 'New principal')
|
|
->assertJsonPath('data.addresses.0.is_main', true)
|
|
->assertJsonPath('data.addresses.1.is_main', false);
|
|
$this->getJson('/api/v1/adminapp/tenant/contact')->assertOk()
|
|
->assertJsonPath('data.addresses.0.address_text', 'New principal');
|
|
$this->assertSame('New principal', $this->tenant->mainAddress()->firstOrFail()->address_text);
|
|
$this->assertSame('8881234', $this->tenant->mainPhoneNumber()->firstOrFail()->local_number);
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', [
|
|
'social_media' => [], 'addresses' => [], 'phone_numbers' => [],
|
|
])->assertOk()->assertJsonCount(0, 'data.phone_numbers');
|
|
$this->assertNull($this->tenant->mainAddress()->first());
|
|
$this->assertNull($this->tenant->mainPhoneNumber()->first());
|
|
}
|
|
|
|
public function test_foreign_ids_and_invalid_payloads_do_not_change_contact(): void
|
|
{
|
|
$this->login();
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertOk();
|
|
$foreign = Address::create(['label' => 'Foreign', 'address_text' => 'Private']);
|
|
$payload = $this->payload();
|
|
$payload['social_media'][0]['url'] = 'https://instagram.com/changed';
|
|
$payload['addresses'][0]['id'] = $foreign->id;
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertUnprocessable()->assertJsonValidationErrors('addresses.0.id');
|
|
$payload = $this->payload();
|
|
$payload['phone_numbers'][0]['id'] = 999999;
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertUnprocessable()->assertJsonValidationErrors('phone_numbers.0.id');
|
|
$payload = $this->payload();
|
|
$payload['social_media'][0]['url'] = 'javascript:alert(1)';
|
|
$payload['addresses'][] = $payload['addresses'][0];
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertUnprocessable()
|
|
->assertJsonValidationErrors(['social_media.0.url']);
|
|
$this->getJson('/api/v1/adminapp/tenant/contact')->assertJsonPath('data.social_media.0.url', 'https://instagram.com/contact');
|
|
}
|
|
|
|
public function test_service_rolls_back_all_changes_if_contact_membership_changes(): void
|
|
{
|
|
$this->login();
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertOk();
|
|
$payload = $this->payload();
|
|
$payload['social_media'] = [];
|
|
$payload['phone_numbers'][0]['id'] = 999999;
|
|
try {
|
|
app(ContactService::class)->update($this->tenant, $payload);
|
|
$this->fail('An invalid contact ID must reject the whole transaction.');
|
|
} catch (ValidationException) {
|
|
$this->assertSame(1, $this->tenant->socialMedia()->count());
|
|
$this->assertSame(1, $this->tenant->addresses()->count());
|
|
}
|
|
}
|
|
|
|
public function test_shared_records_are_not_modified_for_other_tenants(): void
|
|
{
|
|
$this->login();
|
|
$first = $this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertOk()->json('data');
|
|
$other = Tenant::create(['codigo' => 'other-contact', 'nombre' => 'Other', 'dominio' => 'other-contact.test']);
|
|
$other->addresses()->attach($first['addresses'][0]['id'], ['is_main' => true]);
|
|
$other->phoneNumbers()->attach($first['phone_numbers'][0]['id'], ['is_main' => true]);
|
|
$payload = $this->payload();
|
|
$payload['addresses'][0] = [...$payload['addresses'][0], 'id' => $first['addresses'][0]['id'], 'address_text' => 'Changed'];
|
|
$payload['phone_numbers'][0] = [...$payload['phone_numbers'][0], 'id' => $first['phone_numbers'][0]['id'], 'local_number' => '9991234'];
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertOk();
|
|
$this->assertSame('San Martín 123', $other->addresses()->firstOrFail()->address_text);
|
|
$this->assertSame('5551234', $other->phoneNumbers()->firstOrFail()->local_number);
|
|
}
|
|
|
|
public function test_contact_networks_are_independent_of_active_event_networks(): void
|
|
{
|
|
$this->login();
|
|
$event = Event::create(['tenant_code' => $this->tenant->codigo, 'slug' => 'event', 'title' => 'Event']);
|
|
$event->socialMedia()->attach('instagram', ['url' => 'https://instagram.com/event', 'orden' => 0]);
|
|
$this->tenant->update(['active_event_id' => $event->id]);
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $this->payload())->assertOk();
|
|
$this->getJson('/api/v1/adminapp/tenant/contact')->assertOk()
|
|
->assertJsonPath('data.social_media.0.url', 'https://instagram.com/contact');
|
|
$this->assertSame('https://instagram.com/event', $event->socialMedia()->firstOrFail()->pivot->url);
|
|
$tenant = $this->tenant->fresh()->load(['socialMedia', 'activeEvent.socialMedia']);
|
|
$this->assertSame('https://instagram.com/contact', $tenant->socialMedia->first()->pivot->url);
|
|
}
|
|
|
|
private function storefrontData(): array
|
|
{
|
|
$tenant = $this->tenant->fresh()->load(['socialMedia', 'addresses', 'mainAddress', 'phoneNumbers', 'mainPhoneNumber']);
|
|
foreach (['favicon', 'headerLogo', 'footerLogo', 'headerBackgroundImage', 'footerBackgroundImage', 'storefrontWebsiteType'] as $relation) {
|
|
$tenant->setRelation($relation, null);
|
|
}
|
|
|
|
return TenantResource::make($tenant)->resolve(request());
|
|
}
|
|
|
|
public function test_address_coordinates_are_preserved_until_the_address_changes(): void
|
|
{
|
|
$this->login();
|
|
$address = Address::create(['label' => 'Sucursal', 'address_text' => 'Original', 'latitude' => -32.9, 'longitude' => -60.6]);
|
|
$this->tenant->addresses()->attach($address->id, ['is_main' => true]);
|
|
$payload = $this->payload();
|
|
$payload['addresses'] = [['id' => $address->id, 'address_text' => 'Original', 'is_main' => true]];
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertOk()->assertJsonPath('data.addresses.0.latitude', -32.9);
|
|
$payload['addresses'][0]['address_text'] = 'New address';
|
|
$this->putJson('/api/v1/adminapp/tenant/contact', $payload)->assertOk()->assertJsonPath('data.addresses.0.latitude', null);
|
|
$this->assertSame('Sucursal', $address->fresh()->label);
|
|
}
|
|
}
|