Compare commits

...

8 Commits

33 changed files with 1088 additions and 49 deletions

View File

@@ -3,6 +3,7 @@
namespace App\Domains\Commerce\Purchase\Resources; namespace App\Domains\Commerce\Purchase\Resources;
use App\Domains\Commerce\Purchase\Models\PurchaseItem; use App\Domains\Commerce\Purchase\Models\PurchaseItem;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@@ -14,7 +15,11 @@ class PurchaseItemResource extends JsonResource
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
$tenant = $request->route('tenant'); $tenant = $request->route('tenant');
$displayImage = ! $tenant instanceof Tenant || $tenant->display_cart_item_images; $usesEventSummary = $tenant instanceof Tenant
&& $tenant->storefrontWebsiteType?->checkout_summary_type
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
$displayImage = ! $tenant instanceof Tenant
|| (! $usesEventSummary && $tenant->display_cart_item_images);
$imageUrl = $displayImage $imageUrl = $displayImage
? $this->imageAttachment?->getTemporaryUrl(1440) ? $this->imageAttachment?->getTemporaryUrl(1440)
@@ -31,7 +36,7 @@ class PurchaseItemResource extends JsonResource
'nombre' => $this->item_nombre, 'nombre' => $this->item_nombre,
'descripcion' => $this->descripcion, 'descripcion' => $this->descripcion,
'slug' => $this->slug, 'slug' => $this->slug,
'imagen' => $imageUrl, 'imagen' => $this->when(! $usesEventSummary, $imageUrl),
'attributes' => $this->variant_attributes ?? [], 'attributes' => $this->variant_attributes ?? [],
], ],
]; ];

View File

@@ -5,6 +5,9 @@ namespace App\Domains\Commerce\Purchase\Resources;
use App\Domains\Commerce\Purchase\Models\Purchase; use App\Domains\Commerce\Purchase\Models\Purchase;
use App\Domains\Commerce\Purchase\Models\PurchaseItem; use App\Domains\Commerce\Purchase\Models\PurchaseItem;
use App\Domains\Commerce\Purchase\Models\TelepagosPaymentCandidate; use App\Domains\Commerce\Purchase\Models\TelepagosPaymentCandidate;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
@@ -27,6 +30,18 @@ class PurchaseResource extends JsonResource
? (int) $this->resource->getAttribute('tickets_count') ? (int) $this->resource->getAttribute('tickets_count')
: null; : null;
$paymentVerification = $this->resolvePaymentVerification(); $paymentVerification = $this->resolvePaymentVerification();
$tenant = $request->route('tenant');
$usesEventSummary = $tenant instanceof Tenant
&& $tenant->storefrontWebsiteType?->checkout_summary_type
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
/** @var Event|null $checkoutEvent */
$checkoutEvent = $usesEventSummary
? $items
->map(fn (PurchaseItem $item) => $item->sourceCatalogItem?->event)
->filter()
->unique('id')
->first()
: null;
$subtotal = $items->isNotEmpty() $subtotal = $items->isNotEmpty()
? $items->reduce( ? $items->reduce(
@@ -64,6 +79,13 @@ class PurchaseResource extends JsonResource
'email' => $this->email, 'email' => $this->email,
'items_source' => $items->isNotEmpty() ? 'purchase' : null, 'items_source' => $items->isNotEmpty() ? 'purchase' : null,
'items' => PurchaseItemResource::collection($items), 'items' => PurchaseItemResource::collection($items),
'event' => $this->when($usesEventSummary, fn () => $checkoutEvent === null
? null
: [
'id' => $checkoutEvent->id,
'title' => $checkoutEvent->title,
'image' => $checkoutEvent->attachment?->getTemporaryUrl(1440),
]),
'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount), 'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount),
'has_generated_tickets' => $this->when($ticketsCount !== null, $ticketsCount > 0), 'has_generated_tickets' => $this->when($ticketsCount !== null, $ticketsCount > 0),
'payment_verification' => $this->when($paymentVerification !== null, $paymentVerification), 'payment_verification' => $this->when($paymentVerification !== null, $paymentVerification),

View File

@@ -3,12 +3,24 @@
namespace App\Domains\Commerce\Purchase\Services\Checkout; namespace App\Domains\Commerce\Purchase\Services\Checkout;
use App\Domains\Commerce\Purchase\Models\Purchase; use App\Domains\Commerce\Purchase\Models\Purchase;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
class PurchaseResponseLoader class PurchaseResponseLoader
{ {
public function load(Purchase $purchase): Purchase public function load(Purchase $purchase): Purchase
{ {
$relations = ['tenant', 'items.imageAttachment', 'stockReservation']; $purchase->loadMissing('tenant.storefrontWebsiteType');
$usesEventSummary = $purchase->tenant?->storefrontWebsiteType?->checkout_summary_type
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
$relations = [
'tenant',
'items.sourceCatalogItem.event.attachment',
'stockReservation',
];
if (! $usesEventSummary) {
$relations[] = 'items.imageAttachment';
}
if ( if (
$purchase->status === Purchase::STATUS_IN_REVIEW $purchase->status === Purchase::STATUS_IN_REVIEW

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

@@ -7,13 +7,21 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['codigo', 'nombre', 'header_type'])] #[Fillable(['codigo', 'nombre', 'header_type', 'checkout_summary_type'])]
class StorefrontWebsiteType extends Model class StorefrontWebsiteType extends Model
{ {
use HasFactory; use HasFactory;
public const CHECKOUT_SUMMARY_DEFAULT = 'default';
public const CHECKOUT_SUMMARY_EVENT = 'event';
protected $table = 'storefront_website_types'; protected $table = 'storefront_website_types';
protected $attributes = [
'checkout_summary_type' => self::CHECKOUT_SUMMARY_DEFAULT,
];
/** @return HasMany<StorefrontWebsiteTypeExtra, $this> */ /** @return HasMany<StorefrontWebsiteTypeExtra, $this> */
public function extras(): HasMany public function extras(): HasMany
{ {

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,15 @@
namespace App\Domains\Core\Tenant\Resources; namespace App\Domains\Core\Tenant\Resources;
use App\Domains\Commerce\Catalog\Models\Category;
use App\Domains\Core\Menu\Models\Menu;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
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\Attachment;
use App\Shared\Attachable\Models\AttachmentCrop; use App\Shared\Attachable\Models\AttachmentCrop;
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\Tenant\Models\Tenant;
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;
@@ -50,6 +51,8 @@ class TenantResource extends JsonResource
'admin_website_type_code' => $this->admin_website_type_code, 'admin_website_type_code' => $this->admin_website_type_code,
'storefront_website_type_code' => $this->storefront_website_type_code, 'storefront_website_type_code' => $this->storefront_website_type_code,
'header_type' => $this->storefrontWebsiteType?->header_type ?? 'standard', 'header_type' => $this->storefrontWebsiteType?->header_type ?? 'standard',
'checkout_summary_type' => $this->storefrontWebsiteType?->checkout_summary_type
?? StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT,
'active_event_id' => $this->active_event_id, 'active_event_id' => $this->active_event_id,
'event' => $this->whenLoaded('activeEvent', fn () => $this->activeEvent === null 'event' => $this->whenLoaded('activeEvent', fn () => $this->activeEvent === null
? null : $this->formatEvent($this->activeEvent, $eventDateChanges)), ? null : $this->formatEvent($this->activeEvent, $eventDateChanges)),
@@ -99,9 +102,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

@@ -2,9 +2,9 @@
namespace App\Domains\Core\Tenant\Services; namespace App\Domains\Core\Tenant\Services;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Core\Tenant\Models\WebsiteExtra; use App\Domains\Core\Tenant\Models\WebsiteExtra;
use App\Shared\Attachable\Models\Attachment;
use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -16,6 +16,7 @@ class TenantInformationService
'favicon', 'favicon',
'headerBackgroundImage', 'headerBackgroundImage',
'footerBackgroundImage', 'footerBackgroundImage',
'storefrontWebsiteType',
'socialMedia', 'socialMedia',
'websiteExtras.websiteTypeExtra', 'websiteExtras.websiteTypeExtra',
'activeEvent.dates', 'activeEvent.dates',

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

@@ -0,0 +1,27 @@
<?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('storefront_website_types', function (Blueprint $table): void {
$table->string('checkout_summary_type')->default('default')->after('header_type');
});
DB::table('storefront_website_types')
->where('codigo', 'onticket_multi_event')
->update(['checkout_summary_type' => 'event']);
}
public function down(): void
{
Schema::table('storefront_website_types', function (Blueprint $table): void {
$table->dropColumn('checkout_summary_type');
});
}
};

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

@@ -2,8 +2,8 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Domains\Core\Tenant\Services\AdminWebsiteTypeService;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Domains\Core\Tenant\Services\AdminWebsiteTypeService;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use RuntimeException; use RuntimeException;
@@ -56,7 +56,11 @@ class WebsiteTypeSeeder extends Seeder
$shopItStorefront = StorefrontWebsiteType::query()->updateOrCreate( $shopItStorefront = StorefrontWebsiteType::query()->updateOrCreate(
['codigo' => 'shopit'], ['codigo' => 'shopit'],
['nombre' => 'ShopIt', 'header_type' => 'standard'], [
'nombre' => 'ShopIt',
'header_type' => 'standard',
'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT,
],
); );
$shopItStorefront->extras()->updateOrCreate( $shopItStorefront->extras()->updateOrCreate(
@@ -85,12 +89,20 @@ class WebsiteTypeSeeder extends Seeder
$onTicketStorefront = StorefrontWebsiteType::query()->updateOrCreate( $onTicketStorefront = StorefrontWebsiteType::query()->updateOrCreate(
['codigo' => 'onticket'], ['codigo' => 'onticket'],
['nombre' => 'OnTicket - evento único', 'header_type' => 'standard'], [
'nombre' => 'OnTicket - evento único',
'header_type' => 'standard',
'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT,
],
); );
$multiEventStorefront = StorefrontWebsiteType::query()->updateOrCreate( $multiEventStorefront = StorefrontWebsiteType::query()->updateOrCreate(
['codigo' => 'onticket_multi_event'], ['codigo' => 'onticket_multi_event'],
['nombre' => 'OnTicket - múltiples eventos', 'header_type' => 'immersive'], [
'nombre' => 'OnTicket - múltiples eventos',
'header_type' => 'immersive',
'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT,
],
); );
$multiEventStorefront->extras()->updateOrCreate( $multiEventStorefront->extras()->updateOrCreate(

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

@@ -2,14 +2,16 @@
namespace Tests\Feature\Purchase; namespace Tests\Feature\Purchase;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Cart\Models\Cart; use App\Domains\Commerce\Cart\Models\Cart;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Commerce\Catalog\Models\Inventory;
use App\Domains\Commerce\Purchase\Services\CheckoutService; use App\Domains\Commerce\Purchase\Services\CheckoutService;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use App\Shared\Attachable\Enums\AttachmentType;
use App\Shared\Attachable\Models\Attachment;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -117,6 +119,70 @@ class PurchaseCatalogItemTest extends TestCase
]); ]);
} }
public function test_multi_event_checkout_returns_only_the_event_image(): void
{
Storage::fake('s3');
Storage::disk('s3')->buildTemporaryUrlsUsing(
fn (string $path): string => "https://snapshots.test/{$path}",
);
StorefrontWebsiteType::query()->create([
'codigo' => 'onticket_multi_event',
'nombre' => 'OnTicket - múltiples eventos',
'header_type' => 'immersive',
'checkout_summary_type' => StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT,
]);
$tenant = $this->createTenant();
$tenant->update(['storefront_website_type_code' => 'onticket_multi_event']);
$user = User::factory()->create();
$eventImage = $this->createAttachment('checkout-event');
$productImage = $this->createAttachment('checkout-product');
Storage::disk('s3')->put($eventImage->path, 'event-image');
Storage::disk('s3')->put($productImage->path, 'product-image');
$event = Event::query()->create([
'tenant_code' => $tenant->codigo,
'title' => 'Festival de prueba',
'published_at' => now(),
'attachment_id' => $eventImage->id,
]);
$inventory = Inventory::query()->create(['real_stock' => 10]);
$catalogItem = CatalogItem::query()->create([
'tenant_code' => $tenant->codigo,
'inventory_id' => $inventory->id,
'slug' => 'entrada-general',
'nombre' => 'Entrada general',
'precio' => 10000,
]);
$catalogItem->event_id = $event->id;
$catalogItem->save();
$catalogItem->attachments()->attach($productImage->id, ['orden' => 0]);
$cart = Cart::query()->create([
'tenant_codigo' => $tenant->codigo,
'user_id' => $user->id,
'status' => 'active',
]);
$cart->addItem($catalogItem->id, null, 1);
$purchase = app(CheckoutService::class)->startCheckout($tenant, $user->id, [
'cart_id' => $cart->id,
'dni' => '12345678',
'telefono' => '123456789',
'nombre_apellido' => 'Test User',
'email' => 'test@example.com',
]);
$this->actingAs($user, 'sanctum')
->getJson("/api/tenants/{$tenant->codigo}/compras/{$purchase->id}")
->assertOk()
->assertJsonMissingPath('data.items.0.item_details.imagen')
->assertJsonPath('data.event.id', $event->id)
->assertJsonPath('data.event.title', 'Festival de prueba')
->assertJsonPath('data.event.image', "https://snapshots.test/{$eventImage->path}");
}
private function createTenant(): Tenant private function createTenant(): Tenant
{ {
$headerLogo = $this->createAttachment('purchase-header'); $headerLogo = $this->createAttachment('purchase-header');

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,9 +2,9 @@
namespace Tests\Feature\Seeders; namespace Tests\Feature\Seeders;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Core\Tenant\Models\AdminWebsiteType; use App\Domains\Core\Tenant\Models\AdminWebsiteType;
use App\Domains\Core\Tenant\Models\StorefrontWebsiteType; use App\Domains\Core\Tenant\Models\StorefrontWebsiteType;
use App\Shared\Attachable\Models\Attachment;
use Database\Seeders\WebsiteTypeSeeder; use Database\Seeders\WebsiteTypeSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
@@ -54,6 +54,7 @@ class WebsiteTypeSeederTest extends TestCase
Storage::disk('s3')->assertExists($shopIt->favicon->path); Storage::disk('s3')->assertExists($shopIt->favicon->path);
$shopItStorefront = StorefrontWebsiteType::query()->where('codigo', 'shopit')->with('extras')->sole(); $shopItStorefront = StorefrontWebsiteType::query()->where('codigo', 'shopit')->with('extras')->sole();
$this->assertSame('standard', $shopItStorefront->header_type); $this->assertSame('standard', $shopItStorefront->header_type);
$this->assertSame(StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT, $shopItStorefront->checkout_summary_type);
$this->assertSame(['carousel'], $shopItStorefront->extras->pluck('codigo')->all()); $this->assertSame(['carousel'], $shopItStorefront->extras->pluck('codigo')->all());
$this->assertSame('Carrusel principal', $shopItStorefront->extras->sole()->nombre); $this->assertSame('Carrusel principal', $shopItStorefront->extras->sole()->nombre);
$this->assertSame([ $this->assertSame([
@@ -87,8 +88,10 @@ class WebsiteTypeSeederTest extends TestCase
$this->assertSame('onticket_favicon.svg', $onTicket->favicon->filename); $this->assertSame('onticket_favicon.svg', $onTicket->favicon->filename);
$onTicketStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket')->with('extras')->sole(); $onTicketStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket')->with('extras')->sole();
$this->assertSame('standard', $onTicketStorefront->header_type); $this->assertSame('standard', $onTicketStorefront->header_type);
$this->assertSame(StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT, $onTicketStorefront->checkout_summary_type);
$multiEventStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket_multi_event')->with('extras')->sole(); $multiEventStorefront = StorefrontWebsiteType::query()->where('codigo', 'onticket_multi_event')->with('extras')->sole();
$this->assertSame('immersive', $multiEventStorefront->header_type); $this->assertSame('immersive', $multiEventStorefront->header_type);
$this->assertSame(StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT, $multiEventStorefront->checkout_summary_type);
$this->assertEqualsCanonicalizing(['immersiveHeroCarousel', 'immersiveHero'], $multiEventStorefront->extras->pluck('codigo')->all()); $this->assertEqualsCanonicalizing(['immersiveHeroCarousel', 'immersiveHero'], $multiEventStorefront->extras->pluck('codigo')->all());
$this->assertSame($shopItStorefront->extras->sole()->config_schema, $multiEventStorefront->extras->firstWhere('codigo', 'immersiveHeroCarousel')->config_schema); $this->assertSame($shopItStorefront->extras->sole()->config_schema, $multiEventStorefront->extras->firstWhere('codigo', 'immersiveHeroCarousel')->config_schema);
$this->assertEqualsCanonicalizing( $this->assertEqualsCanonicalizing(

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,24 @@ 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', [])
->assertJsonPath('data.checkout_summary_type', StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT);
}
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();
@@ -108,6 +126,7 @@ class BootstrapTenantControllerTest extends TestCase
->assertJsonPath('data.checkout_editing_policy.allow_delete', false) ->assertJsonPath('data.checkout_editing_policy.allow_delete', false)
->assertJsonPath('data.checkout_editing_policy.allow_update_quantity', false) ->assertJsonPath('data.checkout_editing_policy.allow_update_quantity', false)
->assertJsonPath('data.checkout_editing_policy.allow_update_variant', false) ->assertJsonPath('data.checkout_editing_policy.allow_update_variant', false)
->assertJsonPath('data.checkout_summary_type', StorefrontWebsiteType::CHECKOUT_SUMMARY_DEFAULT)
->assertJsonPath('data.display_cart_item_images', false) ->assertJsonPath('data.display_cart_item_images', false)
->assertJsonPath('data.header_bg_color', '#ffffff')->assertJsonPath('data.footer_bg_color', '#ffffff'); ->assertJsonPath('data.header_bg_color', '#ffffff')->assertJsonPath('data.footer_bg_color', '#ffffff');