Compare commits

...

6 Commits

24 changed files with 913 additions and 35 deletions

View File

@@ -37,7 +37,8 @@ class TenantBootstrapService
'roles', 'roles',
fn ($query) => $query->where('codigo', RoleCode::User->value) fn ($query) => $query->where('codigo', RoleCode::User->value)
), ),
'categories' => fn ($query) => $query->orderBy('nombre'), $tenant->storefront_website_type_code === 'onticket_multi_event'
? 'eventCategories' : 'categories' => fn ($query) => $query->orderBy('nombre'),
] ]
); );
} }

View File

@@ -11,6 +11,7 @@ use App\Domains\Core\Client\Models\Client;
use App\Domains\Ticketing\Event\Models\EventDate; use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Event\Models\EventDateChange; use App\Domains\Ticketing\Event\Models\EventDateChange;
use App\Domains\Ticketing\Event\Models\Event; use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Event\Models\EventCategory;
use App\Domains\Core\Menu\Models\Menu; use App\Domains\Core\Menu\Models\Menu;
use App\Domains\Core\Menu\Models\TenantMenu; use App\Domains\Core\Menu\Models\TenantMenu;
use App\Domains\Core\Tenant\Enums\CartEditingPolicy; use App\Domains\Core\Tenant\Enums\CartEditingPolicy;
@@ -265,6 +266,12 @@ class Tenant extends Model
return $this->hasMany(Category::class, 'tenant_code', 'codigo'); return $this->hasMany(Category::class, 'tenant_code', 'codigo');
} }
/** @return HasMany<EventCategory, $this> */
public function eventCategories(): HasMany
{
return $this->hasMany(EventCategory::class, 'tenant_code', 'codigo');
}
/** @return HasMany<ScanAttempt, $this> */ /** @return HasMany<ScanAttempt, $this> */
public function scanAttempts(): HasMany public function scanAttempts(): HasMany
{ {

View File

@@ -2,14 +2,14 @@
namespace App\Domains\Core\Tenant\Resources; namespace App\Domains\Core\Tenant\Resources;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Models\AttachmentCrop;
use App\Domains\Commerce\Catalog\Models\Category; use App\Domains\Commerce\Catalog\Models\Category;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Event\Services\EventDateInfoFormatter;
use App\Domains\Core\Menu\Models\Menu; use App\Domains\Core\Menu\Models\Menu;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Event\Services\EventDateInfoFormatter;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Models\AttachmentCrop;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -99,9 +99,18 @@ class TenantResource extends JsonResource
'menues', 'menues',
fn () => $this->menuTree($this->menues) fn () => $this->menuTree($this->menues)
), ),
'categories' => $this->whenLoaded( 'categories' => $this->when(
'categories', $this->storefront_website_type_code !== 'onticket_multi_event',
fn () => $this->categoryTree($this->categories) fn () => $this->whenLoaded('categories', fn () => $this->categoryTree($this->categories))
),
'event_categories' => $this->when(
$this->storefront_website_type_code === 'onticket_multi_event',
fn () => $this->whenLoaded('eventCategories', fn () => $this->eventCategories
->map(fn ($category) => [
'id' => $category->id,
'nombre' => $category->nombre,
'subcategories' => [],
])->values())
), ),
]; ];
} }

View File

@@ -17,7 +17,12 @@ class PublicEventController extends Controller
abort_unless($tenant->storefront_website_type_code === 'onticket_multi_event', 404); abort_unless($tenant->storefront_website_type_code === 'onticket_multi_event', 404);
$term = trim((string) $request->validated('q', '')); $term = trim((string) $request->validated('q', ''));
return PublicEventResource::collection($events->list($tenant, $term, (int) $request->validated('page', 1))); return PublicEventResource::collection($events->list(
$tenant,
$term,
(int) $request->validated('page', 1),
$request->validated('category_id', null) === null ? null : (int) $request->validated('category_id'),
));
} }
public function show(Tenant $tenant, Event $event, PublicEventService $events): PublicEventResource public function show(Tenant $tenant, Event $event, PublicEventService $events): PublicEventResource

View File

@@ -13,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['tenant_code', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])] #[Fillable(['tenant_code', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])]
class Event extends Model class Event extends Model
{ {
use HasFactory; use HasFactory;
@@ -29,6 +29,12 @@ class Event extends Model
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
} }
/** @return BelongsTo<EventCategory, $this> */
public function eventCategory(): BelongsTo
{
return $this->belongsTo(EventCategory::class);
}
/** @return BelongsTo<Attachment, $this> */ /** @return BelongsTo<Attachment, $this> */
public function attachment(): BelongsTo public function attachment(): BelongsTo
{ {

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Domains\Ticketing\Event\Models;
use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['tenant_code', 'nombre'])]
class EventCategory extends Model
{
/** @return BelongsTo<Tenant, $this> */
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
/** @return HasMany<Event, $this> */
public function events(): HasMany
{
return $this->hasMany(Event::class);
}
}

View File

@@ -11,6 +11,7 @@ class ListPublicEventsRequest extends FormRequest
return [ return [
'q' => ['sometimes', 'string', 'max:120'], 'q' => ['sometimes', 'string', 'max:120'],
'page' => ['sometimes', 'integer', 'min:1'], 'page' => ['sometimes', 'integer', 'min:1'],
'category_id' => ['sometimes', 'integer', 'min:1'],
]; ];
} }
} }

View File

@@ -15,6 +15,7 @@ class PublicEventResource extends JsonResource
{ {
return [ return [
'id' => $this->id, 'id' => $this->id,
'event_category_id' => $this->event_category_id,
'title' => $this->title, 'title' => $this->title,
'subtitle' => $this->subtitle, 'subtitle' => $this->subtitle,
'description' => $this->description, 'description' => $this->description,

View File

@@ -8,12 +8,17 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class PublicEventService class PublicEventService
{ {
public function list(Tenant $tenant, string $term, int $page): LengthAwarePaginator public function list(Tenant $tenant, string $term, int $page, ?int $categoryId = null): LengthAwarePaginator
{ {
$query = $tenant->events() $query = $tenant->events()
->whereNotNull('published_at') ->whereNotNull('published_at')
->where('published_at', '<=', now()); ->where('published_at', '<=', now());
if ($categoryId !== null) {
abort_unless($tenant->eventCategories()->whereKey($categoryId)->exists(), 404);
$query->where('event_category_id', $categoryId);
}
if ($term !== '') { if ($term !== '') {
$query->where(function ($query) use ($term): void { $query->where(function ($query) use ($term): void {
$query->where('title', 'like', '%'.$term.'%') $query->where('title', 'like', '%'.$term.'%')

View File

@@ -0,0 +1,41 @@
<?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::create('event_categories', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code');
$table->string('nombre');
$table->timestamps();
$table->foreign('tenant_code')->references('codigo')->on('tenants')->cascadeOnUpdate()->cascadeOnDelete();
$table->unique(['tenant_code', 'nombre']);
});
Schema::table('events', function (Blueprint $table): void {
$table->foreignId('event_category_id')->nullable()->constrained('event_categories')->nullOnDelete();
});
if (DB::table('tenants')->where('codigo', 'onticket')->exists()) {
$now = now();
foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) {
DB::table('event_categories')->insert([
'tenant_code' => 'onticket', 'nombre' => $nombre,
'created_at' => $now, 'updated_at' => $now,
]);
}
}
}
public function down(): void
{
Schema::table('events', fn (Blueprint $table) => $table->dropConstrainedForeignId('event_category_id'));
Schema::dropIfExists('event_categories');
}
};

View File

@@ -0,0 +1,113 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const TENANT_CODE = 'onticket';
private const EVENT_MENUS = [
'event.index' => [
'label' => 'Eventos',
'route' => '/',
],
'event.detail' => [
'label' => 'Detalle de evento',
'route' => '/evento/:id',
],
];
private const EXCLUDED_MENU_CODES = [
'product.detail',
'help',
'help.faq',
'help.contact',
'help.payment-methods',
'help.shipping',
'help.terms-and-conditions',
'adminapp.tickets',
'adminapp.fiesta-futbol-infantil.entradas',
'adminapp.fiesta-futbol-infantil.alojamientos',
'adminapp.fiesta-futbol-infantil.merchandising',
'adminapp.fiesta-futbol-infantil.comida',
'adminapp.desfile.entradas',
];
public function up(): void
{
$now = now();
DB::transaction(function () use ($now): void {
foreach (self::EVENT_MENUS as $code => $menu) {
DB::table('menues')->updateOrInsert(
['code' => $code],
[
'label' => $menu['label'],
'parent_menu_code' => null,
'content_type' => 'dynamic',
'static_content_schema' => null,
'route' => $menu['route'],
'created_at' => $now,
'updated_at' => $now,
],
);
}
DB::table('roles')
->whereIn('codigo', ['admin', 'adminapp', 'user'])
->pluck('codigo')
->each(function (string $roleCode): void {
foreach (array_keys(self::EVENT_MENUS) as $menuCode) {
DB::table('roles_menues')->updateOrInsert([
'rol_codigo' => $roleCode,
'menu_codigo' => $menuCode,
]);
}
});
if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
return;
}
DB::table('menues')
->whereNotIn('code', self::EXCLUDED_MENU_CODES)
->pluck('code')
->each(function (string $menuCode) use ($now): void {
DB::table('tenants_menues')->updateOrInsert(
[
'tenant_code' => self::TENANT_CODE,
'menu_code' => $menuCode,
],
[
'static_content' => null,
'created_at' => $now,
'updated_at' => $now,
],
);
});
});
}
public function down(): void
{
DB::transaction(function (): void {
$assignedMenuCodes = DB::table('menues')
->whereNotIn('code', self::EXCLUDED_MENU_CODES)
->pluck('code');
DB::table('tenants_menues')
->where('tenant_code', self::TENANT_CODE)
->whereIn('menu_code', $assignedMenuCodes)
->delete();
DB::table('roles_menues')
->whereIn('menu_codigo', array_keys(self::EVENT_MENUS))
->delete();
DB::table('menues')
->whereIn('code', array_keys(self::EVENT_MENUS))
->delete();
});
}
};

View File

@@ -0,0 +1,81 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const MENUS = [
'event.category' => [
'label' => 'Categoría de eventos',
'route' => '/eventos/categoria/:id',
],
'product.category' => [
'label' => 'Categoría de productos',
'route' => '/categoria/:id',
],
];
public function up(): void
{
$now = now();
DB::transaction(function () use ($now): void {
foreach (self::MENUS as $code => $menu) {
DB::table('menues')->updateOrInsert(
['code' => $code],
[
'label' => $menu['label'],
'parent_menu_code' => null,
'content_type' => 'dynamic',
'static_content_schema' => null,
'route' => $menu['route'],
'created_at' => $now,
'updated_at' => $now,
],
);
}
DB::table('roles')
->whereIn('codigo', ['admin', 'adminapp', 'user'])
->pluck('codigo')
->each(function (string $roleCode): void {
foreach (array_keys(self::MENUS) as $menuCode) {
DB::table('roles_menues')->updateOrInsert([
'rol_codigo' => $roleCode,
'menu_codigo' => $menuCode,
]);
}
});
DB::table('tenants')
->pluck('codigo')
->each(function (string $tenantCode) use ($now): void {
$menuCode = $tenantCode === 'onticket' ? 'event.category' : 'product.category';
DB::table('tenants_menues')->updateOrInsert(
[
'tenant_code' => $tenantCode,
'menu_code' => $menuCode,
],
[
'static_content' => null,
'created_at' => $now,
'updated_at' => $now,
],
);
});
});
}
public function down(): void
{
DB::transaction(function (): void {
$menuCodes = array_keys(self::MENUS);
DB::table('tenants_menues')->whereIn('menu_code', $menuCodes)->delete();
DB::table('roles_menues')->whereIn('menu_codigo', $menuCodes)->delete();
DB::table('menues')->whereIn('code', $menuCodes)->delete();
});
}
};

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

View File

@@ -17,6 +17,22 @@ class MenuSeeder extends Seeder
{ {
$menus = [ $menus = [
['code' => 'index', 'label' => 'Inicio', 'route' => '/'], ['code' => 'index', 'label' => 'Inicio', 'route' => '/'],
['code' => 'event.index', 'label' => 'Eventos', 'route' => '/'],
[
'code' => 'event.category',
'label' => 'Categoría de eventos',
'route' => '/eventos/categoria/:id',
],
[
'code' => 'event.detail',
'label' => 'Detalle de evento',
'route' => '/evento/:id',
],
[
'code' => 'product.category',
'label' => 'Categoría de productos',
'route' => '/categoria/:id',
],
[ [
'code' => 'product.detail', 'code' => 'product.detail',
'label' => 'Detalle de producto', 'label' => 'Detalle de producto',
@@ -291,6 +307,14 @@ class MenuSeeder extends Seeder
$desfileMenuCodes = [ $desfileMenuCodes = [
'adminapp.desfile.entradas', 'adminapp.desfile.entradas',
]; ];
$onTicketMenuCodes = [
'event.index',
'event.category',
'event.detail',
];
$catalogMenuCodes = [
'product.category',
];
$frequentlyAskedQuestions = [ $frequentlyAskedQuestions = [
[ [
'pregunta' => '¿Hay algún límite de compra?', 'pregunta' => '¿Hay algún límite de compra?',
@@ -358,6 +382,12 @@ class MenuSeeder extends Seeder
$menuCodes = array_diff($menuCodes, $desfileMenuCodes); $menuCodes = array_diff($menuCodes, $desfileMenuCodes);
} }
if ($tenant->codigo !== 'onticket') {
$menuCodes = array_diff($menuCodes, $onTicketMenuCodes);
} else {
$menuCodes = array_diff($menuCodes, $catalogMenuCodes);
}
// Usar sync para asociar los menues al tenant // Usar sync para asociar los menues al tenant
$tenant->menues()->sync($menuCodes); $tenant->menues()->sync($menuCodes);

View File

@@ -2,13 +2,13 @@
namespace Database\Seeders; 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\Client\Models\Client;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\AdminWebsiteType; 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\TenantService;
use App\Domains\Core\Tenant\Services\WebsiteExtraService; 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\Database\Seeder;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use RuntimeException; use RuntimeException;
@@ -18,6 +18,8 @@ class TenantSeeder extends Seeder
{ {
private const ONTICKET_CLIENT_CODE = 'onticket'; private const ONTICKET_CLIENT_CODE = 'onticket';
private const PYME_RURAL_CLIENT_CODE = 'pyme_rural';
private const SONDER_CLIENT_CODE = 'sonder'; private const SONDER_CLIENT_CODE = 'sonder';
private const SOCIAL_MEDIA = [ private const SOCIAL_MEDIA = [
@@ -60,6 +62,11 @@ class TenantSeeder extends Seeder
['name' => 'OnTicket'], ['name' => 'OnTicket'],
); );
$pymeRuralClient = Client::query()->updateOrCreate(
['code' => self::PYME_RURAL_CLIENT_CODE],
['name' => 'Pyme Rural'],
);
$onTicketType = AdminWebsiteType::query()->where('codigo', 'onticket')->firstOrFail(); $onTicketType = AdminWebsiteType::query()->where('codigo', 'onticket')->firstOrFail();
$onTicketTenant = Tenant::query()->where('codigo', 'onticket')->first(); $onTicketTenant = Tenant::query()->where('codigo', 'onticket')->first();
@@ -76,7 +83,7 @@ class TenantSeeder extends Seeder
'danger_color' => $onTicketType->danger_color, 'danger_color' => $onTicketType->danger_color,
'success_color' => $onTicketType->success_color, 'success_color' => $onTicketType->success_color,
'header_bg_color' => $onTicketType->surface_color, 'header_bg_color' => $onTicketType->surface_color,
'footer_bg_color' => $onTicketType->surface_color, 'footer_bg_color' => $onTicketType->primary_color,
'display_categories' => true, 'display_categories' => true,
'display_seach_bar' => true, 'display_seach_bar' => true,
'admin_website_type_code' => $onTicketType->codigo, 'admin_website_type_code' => $onTicketType->codigo,
@@ -113,6 +120,10 @@ class TenantSeeder extends Seeder
$onTicketTenant = Tenant::query()->where('codigo', 'onticket')->firstOrFail(); $onTicketTenant = Tenant::query()->where('codigo', 'onticket')->firstOrFail();
foreach (['Música', 'Teatro', 'Deportes', 'Infantiles', 'Otros'] as $nombre) {
$onTicketTenant->eventCategories()->firstOrCreate(['nombre' => $nombre]);
}
if ($onTicketTenant->storefront_website_type_code === 'onticket_multi_event' && ! $onTicketTenant->display_seach_bar) { if ($onTicketTenant->storefront_website_type_code === 'onticket_multi_event' && ! $onTicketTenant->display_seach_bar) {
$onTicketTenant->update(['display_seach_bar' => true]); $onTicketTenant->update(['display_seach_bar' => true]);
} }
@@ -146,7 +157,7 @@ class TenantSeeder extends Seeder
'danger_color' => '#FF8888', 'danger_color' => '#FF8888',
'success_color' => '#198754', 'success_color' => '#198754',
'header_bg_color' => '#ffffff', 'header_bg_color' => '#ffffff',
'footer_bg_color' => '#313131', 'footer_bg_color' => '#6376F3',
'display_categories' => true, 'display_categories' => true,
'display_seach_bar' => true, 'display_seach_bar' => true,
'display_cart' => true, 'display_cart' => true,
@@ -195,7 +206,7 @@ class TenantSeeder extends Seeder
->delete(); ->delete();
$this->tenantService->create([ $this->tenantService->create([
'client_id' => $onTicketClient->id, 'client_id' => $pymeRuralClient->id,
'codigo' => 'fiesta_futbol_infantil', 'codigo' => 'fiesta_futbol_infantil',
'nombre' => 'Fiesta Fútbol Infantil', 'nombre' => 'Fiesta Fútbol Infantil',
'dominio' => $fiestaDomain, 'dominio' => $fiestaDomain,
@@ -205,7 +216,7 @@ class TenantSeeder extends Seeder
'danger_color' => '#FF8888', 'danger_color' => '#FF8888',
'success_color' => '#198754', 'success_color' => '#198754',
'header_bg_color' => '#ffffff', 'header_bg_color' => '#ffffff',
'footer_bg_color' => '#015327', 'footer_bg_color' => '#00973F',
'display_categories' => false, 'display_categories' => false,
'display_seach_bar' => false, 'display_seach_bar' => false,
'display_cart' => true, 'display_cart' => true,

View File

@@ -20,6 +20,7 @@ class TestEventsSeeder extends Seeder
$events = [ $events = [
[ [
'title' => '[Prueba] Noche de Música en Vivo', 'title' => '[Prueba] Noche de Música en Vivo',
'category' => 'Música',
'subtitle' => 'Una noche para cantar y bailar', 'subtitle' => 'Una noche para cantar y bailar',
'description' => 'Evento de prueba para explorar la cartelera de OnTicket.', 'description' => 'Evento de prueba para explorar la cartelera de OnTicket.',
'location' => 'Teatro Broadway, Rosario', 'location' => 'Teatro Broadway, Rosario',
@@ -30,6 +31,7 @@ class TestEventsSeeder extends Seeder
], ],
[ [
'title' => '[Prueba] Festival de Sabores', 'title' => '[Prueba] Festival de Sabores',
'category' => 'Otros',
'subtitle' => 'Gastronomía y música para toda la familia', 'subtitle' => 'Gastronomía y música para toda la familia',
'description' => 'Evento de prueba con dos jornadas disponibles.', 'description' => 'Evento de prueba con dos jornadas disponibles.',
'location' => 'Parque de España, Rosario', 'location' => 'Parque de España, Rosario',
@@ -41,6 +43,7 @@ class TestEventsSeeder extends Seeder
], ],
[ [
'title' => '[Prueba] Stand Up en el Centro', 'title' => '[Prueba] Stand Up en el Centro',
'category' => 'Teatro',
'subtitle' => 'Humor para compartir', 'subtitle' => 'Humor para compartir',
'description' => 'Evento de prueba para visualizar distintas propuestas.', 'description' => 'Evento de prueba para visualizar distintas propuestas.',
'location' => 'Centro Cultural La Comedia, Rosario', 'location' => 'Centro Cultural La Comedia, Rosario',
@@ -60,9 +63,16 @@ class TestEventsSeeder extends Seeder
'description' => $definition['description'], 'description' => $definition['description'],
'location' => $definition['location'], 'location' => $definition['location'],
'published_at' => now(), 'published_at' => now(),
'event_category_id' => $tenant->eventCategories()
->where('nombre', $definition['category'])->value('id'),
], ],
); );
if ($event->event_category_id === null) {
$event->update(['event_category_id' => $tenant->eventCategories()
->where('nombre', $definition['category'])->value('id')]);
}
if ($event->attachment_id === null) { if ($event->attachment_id === null) {
$path = public_path('images/tennants/onticket/test-events/'.$definition['image']); $path = public_path('images/tennants/onticket/test-events/'.$definition['image']);

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', '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 ([ foreach ([
'fiesta_futbol_infantil' => 'futbol_infantil_favicon.png', 'fiesta_futbol_infantil' => 'futbol_infantil_favicon.png',
'desfile_pura_tendencia' => 'pura_tendencia_favicon.png', 'desfile_pura_tendencia' => 'pura_tendencia_favicon.png',

View File

@@ -2,12 +2,12 @@
namespace Tests\Feature\Seeders; namespace Tests\Feature\Seeders;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
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\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use Database\Seeders\AuthorizationSeeder; use Database\Seeders\AuthorizationSeeder;
use Database\Seeders\MenuSeeder; use Database\Seeders\MenuSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -183,6 +183,49 @@ class MenuSeederTest extends TestCase
); );
} }
public function test_it_assigns_event_menus_only_to_onticket(): void
{
$onTicket = $this->createTenant('onticket');
$otherTenant = $this->createTenant('other');
$this->seed(AuthorizationSeeder::class);
$this->seed(MenuSeeder::class);
foreach ([
'event.index' => '/',
'event.category' => '/eventos/categoria/:id',
'event.detail' => '/evento/:id',
] as $code => $route) {
$menu = Menu::query()->where('code', $code)->firstOrFail();
$this->assertSame($route, $menu->route);
$this->assertTrue($onTicket->menues()->where('menues.code', $code)->exists());
$this->assertFalse($otherTenant->menues()->where('menues.code', $code)->exists());
$this->assertTrue(
Role::query()
->where('codigo', RoleCode::User->value)
->firstOrFail()
->menus()
->where('menues.code', $code)
->exists()
);
}
$productCategory = Menu::query()->where('code', 'product.category')->firstOrFail();
$this->assertSame('/categoria/:id', $productCategory->route);
$this->assertFalse($onTicket->menues()->where('menues.code', 'product.category')->exists());
$this->assertTrue($otherTenant->menues()->where('menues.code', 'product.category')->exists());
$this->assertTrue(
Role::query()
->where('codigo', RoleCode::User->value)
->firstOrFail()
->menus()
->where('menues.code', 'product.category')
->exists()
);
}
public function test_it_seeds_the_account_menu_hierarchy(): void public function test_it_seeds_the_account_menu_hierarchy(): void
{ {
$tenant = $this->createTenant('account_tenant'); $tenant = $this->createTenant('account_tenant');

View File

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

View File

@@ -2,16 +2,16 @@
namespace Tests\Feature\Tenant; namespace Tests\Feature\Tenant;
use App\Domains\Commerce\Catalog\Models\Category;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Core\Authorization\Models\Role;
use App\Domains\Core\Menu\Models\Menu;
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Shared\Attachable\Enums\AttachmentType; use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment; use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Models\AttachmentCrop; use App\Shared\Attachable\Models\AttachmentCrop;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Core\Authorization\Models\Role;
use App\Domains\Commerce\Catalog\Models\Category;
use App\Domains\Core\Menu\Models\Menu;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use Database\Seeders\WebsiteTypeSeeder; use Database\Seeders\WebsiteTypeSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
@@ -23,6 +23,23 @@ class BootstrapTenantControllerTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_multi_event_bootstrap_returns_event_categories_instead_of_catalog_categories(): void
{
$this->seed(WebsiteTypeSeeder::class);
$tenant = $this->createTenant();
$tenant->update(['storefront_website_type_code' => 'onticket_multi_event']);
$tenant->categories()->create(['nombre' => 'Producto']);
$eventCategory = $tenant->eventCategories()->create(['nombre' => 'Música']);
$this->getJson('/api/tenants/bootstrap?dominio=acme.com&path=%2F')
->assertOk()
->assertJsonMissingPath('data.categories')
->assertJsonCount(1, 'data.event_categories')
->assertJsonPath('data.event_categories.0.id', $eventCategory->id)
->assertJsonPath('data.event_categories.0.nombre', 'Música')
->assertJsonPath('data.event_categories.0.subcategories', []);
}
public function test_it_bootstraps_a_tenant_by_domain(): void public function test_it_bootstraps_a_tenant_by_domain(): void
{ {
$hdrKey = (string) Str::uuid(); $hdrKey = (string) Str::uuid();