Compare commits

..

4 Commits

13 changed files with 387 additions and 1 deletions

View File

@@ -2,9 +2,10 @@
namespace App\Domains\Commerce\Catalog\Models;
use App\Shared\Attachable\Models\Attachment;
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Ticket\Models\Ticket;
use App\Shared\Attachable\Models\Attachment;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -74,6 +75,12 @@ class Variant extends Model
return $this->hasMany(Ticket::class, 'source_variant_id');
}
/** @return HasMany<EntryReservation, $this> */
public function desfileEntryReservations(): HasMany
{
return $this->hasMany(EntryReservation::class);
}
/** @return BelongsTo<Inventory, $this> */
public function inventory(): BelongsTo
{

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Domains\Ticketing\Desfile\Enums;
enum EntryReservationPaymentType: string
{
case Free = 'sin_cargo';
case Other = 'otro_metodo';
public function label(): string
{
return match ($this) {
self::Free => 'Sin cargo',
self::Other => 'Otro método',
};
}
/** @return list<array{value: string, label: string}> */
public static function options(): array
{
return array_map(
fn (self $type): array => [
'value' => $type->value,
'label' => $type->label(),
],
self::cases(),
);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Domains\Ticketing\Desfile\Models;
use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'variant_id',
'fecha_reserva',
'importe',
'tipo_pago',
])]
class EntryReservation extends Model
{
protected $table = 'desfile_entry_reservations';
protected function casts(): array
{
return [
'variant_id' => 'integer',
'fecha_reserva' => 'datetime',
'importe' => 'decimal:2',
'tipo_pago' => EntryReservationPaymentType::class,
];
}
/** @return BelongsTo<Variant, $this> */
public function variant(): BelongsTo
{
return $this->belongsTo(Variant::class)->withTrashed();
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Shared\Forms\Controllers\AdminApp;
use App\Http\Controllers\Controller;
use App\Shared\Forms\Resources\DesfileEntryReservationFormResource;
use App\Shared\Forms\Services\DesfileEntryReservationFormService;
class DesfileEntryReservationFormController extends Controller
{
public function __construct(
private readonly DesfileEntryReservationFormService $formService,
) {}
public function __invoke(): DesfileEntryReservationFormResource
{
return DesfileEntryReservationFormResource::make($this->formService->get());
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Shared\Forms\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class DesfileEntryReservationFormResource extends JsonResource
{
/** @return array<string, mixed> */
public function toArray(Request $request): array
{
return [
'payment_types' => $this->resource['payment_types'],
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Shared\Forms\Services;
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
class DesfileEntryReservationFormService
{
/** @return array{payment_types: list<array{value: string, label: string}>} */
public function get(): array
{
return [
'payment_types' => EntryReservationPaymentType::options(),
];
}
}

View File

@@ -1,5 +1,6 @@
<?php
use App\Shared\Forms\Controllers\AdminApp\DesfileEntryReservationFormController;
use App\Shared\Forms\Controllers\AdminApp\EntryFormController;
use App\Shared\Forms\Controllers\AdminApp\EventFormController;
use App\Shared\Forms\Controllers\AdminApp\FoodFormController;
@@ -14,6 +15,11 @@ Route::prefix('v1/adminapp/forms')
->middleware(['auth:sanctum', 'adminapp.tenant'])
->group(function (): void {
Route::get('event', EventFormController::class);
Route::get(
'desfile/entry-reservation',
DesfileEntryReservationFormController::class
)->middleware('tenant.menu:adminapp.desfile.reservas')
->name('adminapp.forms.desfile.entry-reservation');
Route::get('sale', SaleFormController::class);
Route::get('staff', StaffFormController::class);
Route::get('tickets-filter', TicketFilterFormController::class)

View File

@@ -0,0 +1,82 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const MENU_CODE = 'adminapp.desfile.reservas';
private const TENANT_CODE = 'desfile_pura_tendencia';
public function up(): void
{
if (! DB::table('menues')->where('code', 'main.adminapp')->exists()) {
// Reference data is added by seeders on fresh installations.
return;
}
$now = now();
DB::transaction(function () use ($now): void {
DB::table('menues')->updateOrInsert(
['code' => self::MENU_CODE],
[
'label' => 'Reserva de Tickets',
'parent_menu_code' => 'main.adminapp',
'content_type' => 'dynamic',
'static_content_schema' => null,
'route' => '/admin/desfile/reservas',
'created_at' => $now,
'updated_at' => $now,
],
);
DB::table('tenants_menues')
->where('menu_code', self::MENU_CODE)
->where('tenant_code', '!=', self::TENANT_CODE)
->delete();
if (DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
DB::table('tenants_menues')->updateOrInsert(
[
'tenant_code' => self::TENANT_CODE,
'menu_code' => self::MENU_CODE,
],
[
'static_content' => null,
'created_at' => $now,
'updated_at' => $now,
],
);
}
DB::table('roles')
->whereIn('codigo', ['admin', 'adminapp'])
->pluck('codigo')
->each(function (string $roleCode): void {
DB::table('roles_menues')->updateOrInsert([
'rol_codigo' => $roleCode,
'menu_codigo' => self::MENU_CODE,
]);
});
});
}
public function down(): void
{
DB::transaction(function (): void {
DB::table('tenants_menues')
->where('menu_code', self::MENU_CODE)
->delete();
DB::table('roles_menues')
->where('menu_codigo', self::MENU_CODE)
->delete();
DB::table('menues')
->where('code', self::MENU_CODE)
->delete();
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('desfile_entry_reservations', function (Blueprint $table): void {
$table->id();
$table->foreignId('variant_id')
->constrained('variantes')
->restrictOnDelete();
$table->timestamp('fecha_reserva');
$table->decimal('importe', 10, 2)->nullable();
$table->string('tipo_pago', 24);
$table->timestamps();
$table->index(['fecha_reserva', 'tipo_pago']);
});
}
public function down(): void
{
Schema::dropIfExists('desfile_entry_reservations');
}
};

View File

@@ -130,6 +130,12 @@ class MenuSeeder extends Seeder
'parent_menu_code' => 'main.adminapp',
'route' => '/admin/desfile/entradas',
],
[
'code' => 'adminapp.desfile.reservas',
'label' => 'Reserva de Tickets',
'parent_menu_code' => 'main.adminapp',
'route' => '/admin/desfile/reservas',
],
[
'code' => 'account',
'label' => 'Mi cuenta',
@@ -306,6 +312,7 @@ class MenuSeeder extends Seeder
];
$desfileMenuCodes = [
'adminapp.desfile.entradas',
'adminapp.desfile.reservas',
];
$onTicketMenuCodes = [
'event.index',

View File

@@ -0,0 +1,23 @@
<?php
namespace Tests\Feature\Desfile;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class EntryReservationSchemaTest extends TestCase
{
use RefreshDatabase;
public function test_the_entry_reservations_table_has_the_requested_columns(): void
{
$this->assertTrue(Schema::hasColumns('desfile_entry_reservations', [
'id',
'variant_id',
'fecha_reserva',
'importe',
'tipo_pago',
]));
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Tests\Feature\Forms;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Authorization\Enums\RoleCode;
use App\Domains\Core\Menu\Models\Menu;
use App\Domains\Core\Tenant\Models\Tenant;
use Database\Seeders\AuthorizationSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class AdminAppDesfileEntryReservationFormControllerTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(AuthorizationSeeder::class);
}
public function test_authentication_is_required(): void
{
$this->getJson('/api/v1/adminapp/forms/desfile/entry-reservation')
->assertUnauthorized();
}
public function test_the_tenant_must_have_the_reservations_menu(): void
{
$tenant = $this->createTenant('without_reservations');
Sanctum::actingAs($this->createAdminAppUser($tenant));
$this->getJson('/api/v1/adminapp/forms/desfile/entry-reservation')
->assertNotFound();
}
public function test_it_only_returns_the_available_payment_types(): void
{
$tenant = $this->createTenant('desfile_pura_tendencia');
$this->grantReservationsMenu($tenant);
Sanctum::actingAs($this->createAdminAppUser($tenant));
$this->getJson('/api/v1/adminapp/forms/desfile/entry-reservation')
->assertOk()
->assertExactJson([
'data' => [
'payment_types' => [
['value' => 'sin_cargo', 'label' => 'Sin cargo'],
['value' => 'otro_metodo', 'label' => 'Otro método'],
],
],
]);
}
private function createTenant(string $code): Tenant
{
return Tenant::query()->create([
'codigo' => $code,
'nombre' => str($code)->headline(),
'dominio' => "{$code}.test",
]);
}
private function createAdminAppUser(Tenant $tenant): User
{
return User::factory()->create([
'rol_codigo' => RoleCode::AdminApp->value,
'tenant_codigo' => $tenant->codigo,
]);
}
private function grantReservationsMenu(Tenant $tenant): void
{
$menu = Menu::query()->create([
'code' => 'adminapp.desfile.reservas',
'label' => 'Reserva de Tickets',
'route' => '/admin/desfile/reservas',
]);
$tenant->menues()->attach($menu->code);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Tests\Unit\Desfile;
use App\Domains\Ticketing\Desfile\Enums\EntryReservationPaymentType;
use App\Domains\Ticketing\Desfile\Models\EntryReservation;
use PHPUnit\Framework\TestCase;
class EntryReservationTest extends TestCase
{
public function test_it_exposes_the_available_payment_types(): void
{
$this->assertSame([
['value' => 'sin_cargo', 'label' => 'Sin cargo'],
['value' => 'otro_metodo', 'label' => 'Otro método'],
], EntryReservationPaymentType::options());
}
public function test_it_defines_the_requested_fillable_fields(): void
{
$reservation = new EntryReservation;
$this->assertSame([
'variant_id',
'fecha_reserva',
'importe',
'tipo_pago',
], $reservation->getFillable());
}
}