feat(migrations): add migration for grouping Pyme Rural tenants and updating client associations

This commit is contained in:
2026-09-21 09:35:40 -03:00
parent 9a14b8fbbd
commit 14c81fec1b
8 changed files with 490 additions and 11 deletions

View File

@@ -0,0 +1,93 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const CLIENT_CODE = 'pyme_rural';
private const CLIENT_NAME = 'Pyme Rural';
private const PREVIOUS_CLIENT_CODE = 'onticket';
private const TENANT_CODES = [
'fiesta_futbol_infantil',
'desfile_pura_tendencia',
];
public function up(): void
{
DB::transaction(function (): void {
$clientId = $this->clientId(self::CLIENT_CODE, self::CLIENT_NAME);
$sourceClientIds = DB::table('tenants')
->whereIn('codigo', self::TENANT_CODES)
->pluck('client_id')
->unique();
$this->copyIntegrations($sourceClientIds, $clientId);
DB::table('tenants')
->whereIn('codigo', self::TENANT_CODES)
->update(['client_id' => $clientId]);
});
}
public function down(): void
{
DB::transaction(function (): void {
$previousClientId = $this->clientId(self::PREVIOUS_CLIENT_CODE, 'OnTicket');
$pymeRuralClientId = DB::table('clients')
->where('code', self::CLIENT_CODE)
->value('id');
if ($pymeRuralClientId !== null) {
$this->copyIntegrations(collect([$pymeRuralClientId]), $previousClientId);
}
DB::table('tenants')
->whereIn('codigo', self::TENANT_CODES)
->update(['client_id' => $previousClientId]);
});
}
private function clientId(string $code, string $name): int
{
$clientId = DB::table('clients')->where('code', $code)->value('id');
if ($clientId === null) {
return (int) DB::table('clients')->insertGetId([
'code' => $code,
'name' => $name,
'created_at' => now(),
'updated_at' => now(),
]);
}
DB::table('clients')->where('id', $clientId)->update([
'name' => $name,
'updated_at' => now(),
]);
return (int) $clientId;
}
private function copyIntegrations(iterable $sourceClientIds, int $targetClientId): void
{
foreach ($sourceClientIds as $sourceClientId) {
DB::table('client_integrations')
->where('client_id', $sourceClientId)
->orderBy('id')
->get()
->each(function (object $integration) use ($targetClientId): void {
DB::table('client_integrations')->insertOrIgnore([
'client_id' => $targetClientId,
'integration_code' => $integration->integration_code,
'integration_instance_id' => $integration->integration_instance_id,
'created_at' => $integration->created_at,
'updated_at' => $integration->updated_at,
]);
});
}
}
};

View File

@@ -12,9 +12,9 @@ use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\FeaturedGroup;
use App\Domains\Commerce\Catalog\Services\CatalogService;
use App\Domains\Core\Client\Models\Client;
use App\Domains\Ticketing\Desfile\Services\InvitationPurchaseProvisioner;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Services\TenantService;
use App\Domains\Ticketing\Desfile\Services\InvitationPurchaseProvisioner;
use Illuminate\Database\Seeder;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
@@ -22,7 +22,7 @@ use RuntimeException;
class DesfilePuraTendenciaSeeder extends Seeder
{
private const ONTICKET_CLIENT_CODE = 'onticket';
private const PYME_RURAL_CLIENT_CODE = 'pyme_rural';
private const TENANT_CODE = 'desfile_pura_tendencia';
@@ -34,9 +34,9 @@ class DesfilePuraTendenciaSeeder extends Seeder
public function run(): void
{
$client = Client::query()->firstOrCreate(
['code' => self::ONTICKET_CLIENT_CODE],
['name' => 'OnTicket'],
$client = Client::query()->updateOrCreate(
['code' => self::PYME_RURAL_CLIENT_CODE],
['name' => 'Pyme Rural'],
);
$tenant = Tenant::query()->where('codigo', self::TENANT_CODE)->first();

View File

@@ -2,13 +2,13 @@
namespace Database\Seeders;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Services\AttachmentService;
use App\Domains\Core\Client\Models\Client;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Services\TenantService;
use App\Domains\Core\Tenant\Services\WebsiteExtraService;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Services\AttachmentService;
use Illuminate\Database\Seeder;
use Illuminate\Http\UploadedFile;
use RuntimeException;
@@ -18,6 +18,8 @@ class TenantSeeder extends Seeder
{
private const ONTICKET_CLIENT_CODE = 'onticket';
private const PYME_RURAL_CLIENT_CODE = 'pyme_rural';
private const SONDER_CLIENT_CODE = 'sonder';
private const SOCIAL_MEDIA = [
@@ -60,6 +62,11 @@ class TenantSeeder extends Seeder
['name' => 'OnTicket'],
);
$pymeRuralClient = Client::query()->updateOrCreate(
['code' => self::PYME_RURAL_CLIENT_CODE],
['name' => 'Pyme Rural'],
);
$onTicketType = AdminWebsiteType::query()->where('codigo', 'onticket')->firstOrFail();
$onTicketTenant = Tenant::query()->where('codigo', 'onticket')->first();
@@ -199,7 +206,7 @@ class TenantSeeder extends Seeder
->delete();
$this->tenantService->create([
'client_id' => $onTicketClient->id,
'client_id' => $pymeRuralClient->id,
'codigo' => 'fiesta_futbol_infantil',
'nombre' => 'Fiesta Fútbol Infantil',
'dominio' => $fiestaDomain,

View File

@@ -0,0 +1,130 @@
<?php
error_reporting(error_reporting() & ~(E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING | E_DEPRECATED | E_USER_DEPRECATED));
class LspHelper
{
public static function relativePath($path)
{
if (!str_contains($path, base_path())) {
return (string) $path;
}
return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR);
}
public static function isVendor($path)
{
return str_contains($path, base_path('vendor'));
}
public static function propertyDefault(ReflectionProperty $property, ?ReflectionParameter $parameter = null): array
{
if ($property->hasDefaultValue()) {
return ['default' => $property->getDefaultValue()];
}
if ($parameter?->isDefaultValueAvailable()) {
return ['default' => $parameter->getDefaultValue()];
}
return [];
}
public static function formatDefaultValue(mixed $value): mixed
{
return match (true) {
is_array($value) => 'array(...)',
$value instanceof UnitEnum => get_class($value) . '::' . $value->name,
$value instanceof Closure => 'Closure',
is_object($value) => get_class($value),
is_string($value) => var_export($value, true),
is_null($value) => 'null',
is_bool($value) => $value ? 'true' : 'false',
default => $value,
};
}
}
use Pest\Expectation;
use Pest\TestSuite;
$pest = new class
{
public function __construct()
{
if ($this->isInstalled()) {
$this->boot();
}
}
public function isInstalled(): bool
{
return class_exists(TestSuite::class);
}
protected function boot(): void
{
require_once base_path('vendor/pestphp/pest/overrides/Runner/TestSuiteLoader.php');
TestSuite::getInstance(base_path(), 'tests');
if (file_exists($pestFile = base_path('tests/Pest.php'))) {
require_once $pestFile;
}
}
public function config(): ?array
{
if (!$this->isInstalled()) {
return null;
}
return [
'uses' => $this->uses(),
'expectations' => $this->expectations(),
];
}
protected function uses(): array
{
if (is_null($instance = TestSuite::getInstance())) {
return [];
}
$reflection = new ReflectionProperty($instance->tests, 'uses');
$uses = $reflection->getValue($instance->tests);
return collect($uses)->map(function (array $use, string $path) {
[$classOrTraits] = $use;
return [
'path' => LspHelper::relativePath($path),
'classes' => array_values(array_filter($classOrTraits, fn ($c) => class_exists($c))),
'traits' => array_values(array_filter($classOrTraits, fn ($c) => trait_exists($c))),
];
})->values()->all();
}
protected function expectations(): array
{
$reflection = new ReflectionProperty(Expectation::class, 'extends');
$extends = $reflection->getValue();
return collect($extends)->map(function (Closure $closure, string $name) {
$parameters = collect((new ReflectionFunction($closure))->getParameters())
->map(function (ReflectionParameter $param) {
$type = $param->hasType() ? $param->getType() . ' ' : '';
$default = $param->isOptional() && $param->isDefaultValueAvailable()
? ' = ' . var_export($param->getDefaultValue(), true)
: '';
return $type . '$' . $param->getName() . $default;
})
->join(', ');
return compact('name', 'parameters');
})->values()->all();
}
};
echo json_encode($pest->config());

View File

@@ -0,0 +1,130 @@
<?php
error_reporting(error_reporting() & ~(E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING | E_DEPRECATED | E_USER_DEPRECATED));
class LspHelper
{
public static function relativePath($path)
{
if (!str_contains($path, base_path())) {
return (string) $path;
}
return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR);
}
public static function isVendor($path)
{
return str_contains($path, base_path('vendor'));
}
public static function propertyDefault(ReflectionProperty $property, ?ReflectionParameter $parameter = null): array
{
if ($property->hasDefaultValue()) {
return ['default' => $property->getDefaultValue()];
}
if ($parameter?->isDefaultValueAvailable()) {
return ['default' => $parameter->getDefaultValue()];
}
return [];
}
public static function formatDefaultValue(mixed $value): mixed
{
return match (true) {
is_array($value) => 'array(...)',
$value instanceof UnitEnum => get_class($value) . '::' . $value->name,
$value instanceof Closure => 'Closure',
is_object($value) => get_class($value),
is_string($value) => var_export($value, true),
is_null($value) => 'null',
is_bool($value) => $value ? 'true' : 'false',
default => $value,
};
}
}
use Pest\Expectation;
use Pest\TestSuite;
$pest = new class
{
public function __construct()
{
if ($this->isInstalled()) {
$this->boot();
}
}
public function isInstalled(): bool
{
return class_exists(TestSuite::class);
}
protected function boot(): void
{
require_once base_path('vendor/pestphp/pest/overrides/Runner/TestSuiteLoader.php');
TestSuite::getInstance(base_path(), 'tests');
if (file_exists($pestFile = base_path('tests/Pest.php'))) {
require_once $pestFile;
}
}
public function config(): ?array
{
if (!$this->isInstalled()) {
return null;
}
return [
'uses' => $this->uses(),
'expectations' => $this->expectations(),
];
}
protected function uses(): array
{
if (is_null($instance = TestSuite::getInstance())) {
return [];
}
$reflection = new ReflectionProperty($instance->tests, 'uses');
$uses = $reflection->getValue($instance->tests);
return collect($uses)->map(function (array $use, string $path) {
[$classOrTraits] = $use;
return [
'path' => LspHelper::relativePath($path),
'classes' => array_values(array_filter($classOrTraits, fn ($c) => class_exists($c))),
'traits' => array_values(array_filter($classOrTraits, fn ($c) => trait_exists($c))),
];
})->values()->all();
}
protected function expectations(): array
{
$reflection = new ReflectionProperty(Expectation::class, 'extends');
$extends = $reflection->getValue();
return collect($extends)->map(function (Closure $closure, string $name) {
$parameters = collect((new ReflectionFunction($closure))->getParameters())
->map(function (ReflectionParameter $param) {
$type = $param->hasType() ? $param->getType() . ' ' : '';
$default = $param->isOptional() && $param->isDefaultValueAvailable()
? ' = ' . var_export($param->getDefaultValue(), true)
: '';
return $type . '$' . $param->getName() . $default;
})
->join(', ');
return compact('name', 'parameters');
})->values()->all();
}
};
echo json_encode($pest->config());

View File

@@ -0,0 +1,109 @@
<?php
namespace Tests\Feature\Migrations;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class GroupPymeRuralTenantsTest extends TestCase
{
public function test_it_groups_the_futbol_and_desfile_tenants_under_pyme_rural(): void
{
$this->createSchema();
$now = now();
$onTicketClientId = DB::table('clients')->insertGetId([
'code' => 'onticket',
'name' => 'OnTicket',
'created_at' => $now,
'updated_at' => $now,
]);
DB::table('tenants')->insert([
['client_id' => $onTicketClientId, 'codigo' => 'onticket'],
['client_id' => $onTicketClientId, 'codigo' => 'fiesta_futbol_infantil'],
['client_id' => $onTicketClientId, 'codigo' => 'desfile_pura_tendencia'],
]);
DB::table('integrations')->insert([
'integration_code' => 'email',
'name' => 'Email',
]);
$instanceId = DB::table('integration_instances')->insertGetId([
'integration_code' => 'email',
'name' => 'OnTicket email',
'integration_data' => null,
'created_at' => $now,
'updated_at' => $now,
]);
DB::table('client_integrations')->insert([
'client_id' => $onTicketClientId,
'integration_code' => 'email',
'integration_instance_id' => $instanceId,
'created_at' => $now,
'updated_at' => $now,
]);
$migration = require database_path(
'migrations/2026_09_21_020000_group_pyme_rural_tenants.php'
);
$migration->up();
$pymeRuralClient = DB::table('clients')->where('code', 'pyme_rural')->sole();
$this->assertSame('Pyme Rural', $pymeRuralClient->name);
$this->assertSame(2, DB::table('tenants')
->whereIn('codigo', ['fiesta_futbol_infantil', 'desfile_pura_tendencia'])
->where('client_id', $pymeRuralClient->id)
->count());
$this->assertSame($onTicketClientId, DB::table('tenants')
->where('codigo', 'onticket')
->value('client_id'));
$this->assertDatabaseHas('client_integrations', [
'client_id' => $pymeRuralClient->id,
'integration_code' => 'email',
'integration_instance_id' => $instanceId,
]);
$migration->down();
$this->assertSame(2, DB::table('tenants')
->whereIn('codigo', ['fiesta_futbol_infantil', 'desfile_pura_tendencia'])
->where('client_id', $onTicketClientId)
->count());
}
private function createSchema(): void
{
Schema::create('clients', function (Blueprint $table): void {
$table->id();
$table->string('code')->unique();
$table->string('name');
$table->timestamps();
});
Schema::create('tenants', function (Blueprint $table): void {
$table->id();
$table->foreignId('client_id');
$table->string('codigo')->unique();
});
Schema::create('integrations', function (Blueprint $table): void {
$table->string('integration_code')->unique();
$table->string('name');
});
Schema::create('integration_instances', function (Blueprint $table): void {
$table->id();
$table->string('integration_code');
$table->string('name');
$table->longText('integration_data')->nullable();
$table->timestamps();
});
Schema::create('client_integrations', function (Blueprint $table): void {
$table->id();
$table->foreignId('client_id');
$table->string('integration_code');
$table->foreignId('integration_instance_id');
$table->timestamps();
$table->unique(['client_id', 'integration_code']);
});
}
}

View File

@@ -55,6 +55,14 @@ class DesfilePuraTendenciaSeederTest extends TestCase
'site_title' => 'Fiesta Fútbol Infantil',
]);
$pymeRuralClientId = DB::table('clients')->where('code', 'pyme_rural')->value('id');
$this->assertNotNull($pymeRuralClientId);
$this->assertSame('Pyme Rural', DB::table('clients')->where('id', $pymeRuralClientId)->value('name'));
$this->assertSame(2, DB::table('tenants')
->whereIn('codigo', ['fiesta_futbol_infantil', 'desfile_pura_tendencia'])
->where('client_id', $pymeRuralClientId)
->count());
foreach ([
'fiesta_futbol_infantil' => 'futbol_infantil_favicon.png',
'desfile_pura_tendencia' => 'pura_tendencia_favicon.png',

View File

@@ -2,9 +2,9 @@
namespace Tests\Feature\Seeders;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Shared\Attachable\Models\Attachment;
use Database\Seeders\SocialMediaSeeder;
use Database\Seeders\TenantSeeder;
use Database\Seeders\WebsiteTypeSeeder;
@@ -150,6 +150,8 @@ class TenantSeederTest extends TestCase
->with('websiteExtras.websiteTypeExtra')
->sole();
$this->assertSame('pyme_rural', $fiesta->client->code);
$this->assertSame('Pyme Rural', $fiesta->client->name);
$this->assertSame('onticket', $fiesta->storefront_website_type_code);
$this->assertSame('onticket', $fiesta->admin_website_type_code);
$this->assertSame('full', $fiesta->cart_editing_policy->value);